从入门到实战:彻底讲透 Python `threading` 的工作原理、正确用法与避坑指南

从入门到实战:彻底讲透 Python threading 的工作原理、正确用法与避坑指南
很多人第一次接触 Python 多线程,都会有一种“会用,但没真正懂”的感觉。
明明开了好几个线程,程序却不一定更快;
明明逻辑很简单,却偶尔出现莫名其妙的数据错误;
明明线程都start()了,程序还是卡住不退出。这篇文章就来把 Python
threading一次讲透。
不只是“怎么写”,更重要的是“为什么这样写”。
目录
一、为什么要学 threading
在 Python 里,threading 是最经典、最常见的并发编程工具之一。
很多业务场景里,它都特别实用,比如:
- 🌐 同时发起多个网络请求
- 📁 同时处理多个文件读写任务
- 🖥️ GUI 程序中避免界面卡死
- 🧾 日志上报、埋点、消息推送等后台任务
- 🔄 生产者-消费者模型
- 🚦 控制资源访问顺序或并发数量
一句话概括:
只要你的任务“多数时间在等待”,多线程通常就有发挥空间。
这里的“等待”包括:
- 等网络响应
- 等磁盘 IO
- 等数据库返回
- 等第三方接口
- 等队列消息
- 等用户输入
这也是理解 threading 的第一把钥匙。
二、什么是线程?和进程有什么区别?
先别急着写代码,先把概念捋顺。
1. 进程是什么
进程(Process)可以理解为:
一个正在运行的程序实例。
比如你双击启动一个 Python 程序,操作系统就会为它创建一个进程。这个进程有自己独立的:
- 内存空间
- 文件描述符
- 资源上下文
- 地址空间
2. 线程是什么
线程(Thread)是:
进程内部的执行单元。
一个进程里可以有多个线程,这些线程共享同一个进程的大部分资源,比如:
- 代码段
- 堆内存
- 全局变量
- 打开的文件资源
但每个线程也有自己独立的:
- 程序计数器
- 寄存器上下文
- 栈空间
3. 进程 vs 线程
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源隔离 | 强 | 弱 |
| 创建成本 | 高 | 低 |
| 切换成本 | 高 | 较低 |
| 数据共享 | 麻烦 | 容易 |
| 安全性 | 更高 | 更容易出错 |
| 通信方式 | IPC | 直接共享内存 |
4. 一个非常形象的类比
可以把进程想象成一家工厂,把线程想象成工厂里的工人:
- 工厂(进程)有自己的厂房和设备
- 工人(线程)共享这些设备
- 工人之间协作更方便
- 但如果没有管理好,也更容易“撞车”
所以线程的核心特点是:
轻量、共享、协作方便,但需要额外解决同步问题。
三、Python 为什么需要 threading
因为现实世界里,很多程序都不是单纯地“计算”,而是在做大量等待。
举个例子:
你要请求 20 个接口,每个接口平均等待 1 秒。
如果串行执行,大概需要:
20 * 1 = 20 秒
如果你同时起 20 个线程去请求,那么整体耗时会接近:
≈ 1 秒多一点
因为这些线程大部分时间都在“等响应”,CPU 正好可以切换去执行别的线程。
所以:
✅ 对于 IO 密集型任务,多线程非常有价值。
四、先建立正确认知:Python 多线程不一定让程序更快
这是很多人最容易误解的一点。
1. 多线程不是“万能加速器”
很多人学完多线程之后,会下意识觉得:
“我把任务拆成多个线程,程序就会更快。”
这句话并不总成立。
Python 中的 threading 更适合:
- IO 密集型任务
- 等待时间远多于计算时间的任务
而不适合:
- 大量纯计算任务
- CPU 持续满负载的任务
2. 为什么?
因为 Python 里有一个经典机制:GIL(全局解释器锁)。
后面我们会详细讲,但你现在先记住一句:
在 CPython 中,同一时刻只能有一个线程执行 Python 字节码。
这意味着:
- 多线程做 IO,通常有效
- 多线程做 CPU 密集计算,通常没明显收益,甚至更慢
五、threading 模块快速上手
Python 标准库自带 threading,不需要安装。
import threading
1. 最简单的线程示例
import threading
import time
def worker():
print(f"线程 {threading.current_thread().name} 开始执行")
time.sleep(2)
print(f"线程 {threading.current_thread().name} 执行结束")
t = threading.Thread(target=worker, name="Worker-1")
t.start()
print("主线程继续往下执行")
t.join()
print("主线程结束")
输出大概类似:
线程 Worker-1 开始执行
主线程继续往下执行
线程 Worker-1 执行结束
主线程结束
这里发生了什么?
Thread(...)创建线程对象target=worker指定线程要执行的函数start()启动线程- 主线程不会等它,继续往下执行
join()表示主线程等待子线程执行完
2. 使用函数创建线程
这是最常见、最直接的方式。
import threading
def task(task_id, username):
print(f"任务ID: {task_id}, 用户: {username}, 当前线程: {threading.current_thread().name}")
t = threading.Thread(target=task, args=(1001, "alex"), name="TaskThread")
t.start()
t.join()
参数说明
| 参数 | 作用 |
|---|---|
target | 线程执行的目标函数 |
args | 位置参数元组 |
kwargs | 关键字参数字典 |
name | 线程名称 |
daemon | 是否为守护线程 |
比如:
t = threading.Thread(
target=task,
args=(1001, "alex"),
daemon=False,
name="TaskThread"
)
3. 继承 Thread 类创建线程
如果你的线程逻辑比较复杂,或者希望把线程的状态封装起来,继承 Thread 是更好的方式。
import threading
import time
class DownloadThread(threading.Thread):
def __init__(self, url):
super().__init__()
self.url = url
def run(self):
print(f"{self.name} 开始下载: {self.url}")
time.sleep(2)
print(f"{self.name} 下载完成: {self.url}")
t1 = DownloadThread("https://example.com/a")
t2 = DownloadThread("https://example.com/b")
t1.start()
t2.start()
t1.join()
t2.join()
什么时候适合继承?
适合这些场景:
- 线程逻辑本身就是一个独立对象
- 线程有状态,比如任务 ID、URL、结果、异常信息
- 想更面向对象地组织代码
什么时候不适合?
如果只是简单调用一个函数,那就没必要为了“面向对象”而强行继承,反而更重。
六、线程对象的核心属性与方法
下面这些是日常写多线程最常用的内容。
| 名称 | 类型 | 说明 |
|---|---|---|
start() | 方法 | 启动线程 |
run() | 方法 | 线程执行逻辑 |
join() | 方法 | 等待线程结束 |
is_alive() | 方法 | 线程是否仍在运行 |
name | 属性 | 线程名 |
daemon | 属性 | 是否为守护线程 |
ident | 属性 | 线程标识符 |
native_id | 属性 | OS 层线程 ID(Python 3.8+) |
示例:
import threading
import time
def worker():
time.sleep(1)
t = threading.Thread(target=worker, name="DemoThread")
print(t.is_alive()) # False
t.start()
print(t.is_alive()) # True
t.join()
print(t.is_alive()) # False
七、线程的执行流程:start()、run()、join() 到底是什么关系?
这是一个很基础,但非常容易被忽视的问题。
1. start() 是启动线程
调用 start() 后,Python 会真正创建一个新的线程,并在新线程里调用 run()。
t.start()
2. run() 是线程执行体
默认情况下,run() 会去执行你传入的 target 函数。
如果你是继承 Thread,那通常就是重写这个方法。
3. join() 是等待线程结束
它不会“启动线程”,而是让当前线程阻塞,直到目标线程结束。
4. 为什么不能直接调 run()?
因为直接调用:
t.run()
这不是多线程执行,而只是普通函数调用,代码仍然在当前线程里跑。
这点特别重要 ⚠️
来看示例:
import threading
def worker():
print(f"当前线程: {threading.current_thread().name}")
t = threading.Thread(target=worker, name="SubThread")
t.run() # 普通方法调用,不会启动新线程
输出很可能是:
当前线程: MainThread
而不是 SubThread。
八、线程共享数据:方便,但危险
线程最大的优势之一,就是共享同一进程的内存空间。
例如:
import threading
counter = 0
def worker():
global counter
counter += 1
threads = []
for _ in range(100):
t = threading.Thread(target=worker)
threads.append(t)
t.start()
for t in threads:
t.join()
print(counter)
你可能以为结果一定是 100,但实际上不一定。
为什么?
因为:
counter += 1
看起来像“一步”,实际上不是原子操作。
它大致可以拆成:
- 读取
counter - 计算
counter + 1 - 写回
counter
如果两个线程交叉执行,就可能发生数据覆盖。
这就是经典的:竞态条件(Race Condition)。
九、线程安全到底是什么?
所谓线程安全,可以简单理解为:
多个线程同时访问某段代码或某份数据时,结果仍然是正确且可预期的。
线程不安全,通常会出现这些问题:
- 结果偶发错误
- 同样的代码多跑几次结果不一样
- 某些 bug 很难复现
- 程序偶尔死锁、卡住、数据错乱
一个经典错误示例
import threading
counter = 0
def add():
global counter
for _ in range(100000):
counter += 1
threads = [threading.Thread(target=add) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter)
理论上结果应该是:
200000
但实际可能小于这个值。
这就说明:
共享可变数据 + 无同步控制 = 高风险。
十、锁:Lock 的本质与使用场景
想解决共享数据竞争问题,最基础的办法就是:加锁。
1. 什么是锁
锁的本质是:
让某一段代码在同一时刻只允许一个线程进入。
这段代码通常叫:临界区(Critical Section)。
2. Lock 基本用法
import threading
counter = 0
lock = threading.Lock()
def add():
global counter
for _ in range(100000):
with lock:
counter += 1
threads = [threading.Thread(target=add) for _ in range(2)]
for t in threads:
t.start()
for t in threads:
t.join()
print(counter)
这时候结果就稳定了。
3. acquire() / release() 写法
除了 with,也可以手动写:
lock.acquire()
try:
counter += 1
finally:
lock.release()
推荐结论
✅ 优先使用 with lock:
因为更安全、更简洁,也不容易忘记释放锁。
4. 锁会带来什么代价?
任何同步机制都不是免费的。
加锁会带来:
- 上下文切换开销
- 等待开销
- 代码复杂度上升
- 死锁风险
所以一个很重要的原则是:
锁要加在必要的位置,而且范围尽可能小。
不好的写法
with lock:
do_something()
do_other_thing()
time.sleep(2)
如果 sleep(2) 没必要放在锁里,这就会导致别的线程白白等待。
更合理的写法
with lock:
update_shared_data()
time.sleep(2)
十一、可重入锁:RLock 解决什么问题?
普通 Lock 有一个特点:
- 同一个线程拿到锁之后
- 如果它自己再次申请这把锁
- 会把自己卡住
示例:
import threading
lock = threading.Lock()
def func_a():
with lock:
func_b()
def func_b():
with lock:
print("执行 func_b")
这段代码可能死锁。
因为 func_a() 已经拿了锁,进入 func_b() 后又申请同一把锁,而普通锁不允许同一线程重复获取。
这时候就要用 RLock(Reentrant Lock,可重入锁)。
import threading
lock = threading.RLock()
def func_a():
with lock:
func_b()
def func_b():
with lock:
print("执行 func_b")
Lock 和 RLock 对比
| 对比项 | Lock | RLock |
|---|---|---|
| 同一线程重复加锁 | 不允许 | 允许 |
| 性能 | 略高 | 略低 |
| 使用场景 | 普通互斥 | 嵌套调用、递归调用 |
实战建议
- 默认优先用
Lock - 确实存在“同线程重复进入锁区”时再考虑
RLock
十二、条件变量:Condition 如何实现“等待通知”
当线程之间不是单纯抢资源,而是需要“等某个条件成立”时,Lock 就不够优雅了。
这时候可以用 Condition。
1. 典型场景
- 队列为空,消费者要等待
- 资源未准备好,工作线程要等待
- 某个状态满足后,再通知其他线程继续执行
2. 基本模型
- 一个线程
wait() - 另一个线程
notify()或notify_all()
3. 示例
import threading
import time
condition = threading.Condition()
ready = False
def consumer():
global ready
with condition:
while not ready:
print("消费者:条件不满足,进入等待")
condition.wait()
print("消费者:条件满足,开始执行")
def producer():
global ready
time.sleep(2)
with condition:
ready = True
print("生产者:条件已准备好,发出通知")
condition.notify()
t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=producer)
t1.start()
t2.start()
t1.join()
t2.join()
4. 为什么 wait() 外面要配合 while?
很多初学者会写成:
if not ready:
condition.wait()
但更推荐:
while not ready:
condition.wait()
原因有两个:
- 可能被伪唤醒
- 被唤醒后,条件也不一定仍然成立
所以这是经典写法:
with condition:
while not 条件:
condition.wait()
十三、信号量:Semaphore 适合控制并发数量
有些场景不是“只能一个线程进入”,而是“最多允许 N 个线程同时进入”。
比如:
- 同时最多允许 3 个线程下载
- 同时最多允许 10 个线程调用数据库
- 同时最多允许 5 个线程访问某个第三方接口
这时用 Semaphore 非常合适。
示例
import threading
import time
semaphore = threading.Semaphore(3)
def worker(i):
with semaphore:
print(f"线程 {i} 进入执行")
time.sleep(2)
print(f"线程 {i} 执行结束")
threads = []
for i in range(10):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
效果
虽然开了 10 个线程,但任意时刻只有 3 个线程真正进入执行区。
十四、事件对象:Event 做线程间通知非常好用
如果你的需求只是:
- 一个线程等通知
- 另一个线程发通知
- 不需要复杂的锁条件管理
那么 Event 往往比 Condition 更轻量、更直观。
常用方法
| 方法 | 说明 |
|---|---|
set() | 设置事件为已触发 |
clear() | 重置事件 |
wait() | 等待事件触发 |
is_set() | 判断是否已触发 |
示例
import threading
import time
event = threading.Event()
def worker():
print("工作线程:等待开始信号")
event.wait()
print("工作线程:收到信号,开始执行")
def controller():
time.sleep(2)
print("控制线程:发出开始信号")
event.set()
t1 = threading.Thread(target=worker)
t2 = threading.Thread(target=controller)
t1.start()
t2.start()
t1.join()
t2.join()
适用场景
- 程序启动协调
- 多线程统一开始
- 停止信号广播
- 某个资源准备完成后通知多个线程
十五、队列:线程通信最推荐的方式
如果你真的要在多线程之间传数据,首选通常不是共享列表或字典,而是 queue.Queue。
为什么?
因为它天然线程安全。
1. 为什么推荐队列
相比手动管理共享变量:
- 更安全
- 更清晰
- 更符合生产者-消费者模型
- 不容易写出难排查的并发 bug
2. 基本示例
import threading
import queue
import time
q = queue.Queue()
def producer():
for i in range(5):
print(f"生产者放入: {i}")
q.put(i)
time.sleep(1)
q.put(None) # 结束标记
def consumer():
while True:
item = q.get()
if item is None:
print("消费者结束")
break
print(f"消费者取出: {item}")
q.task_done()
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()
3. Queue 常用方法
| 方法 | 说明 |
|---|---|
put(item) | 放入数据 |
get() | 取出数据 |
task_done() | 标记任务处理完成 |
join() | 等待所有任务完成 |
put_nowait() | 非阻塞放入 |
get_nowait() | 非阻塞取出 |
4. 一个核心建议
✅ 线程之间通信,优先考虑 Queue。
这是非常重要的工程经验。
因为你越少共享可变状态,线程安全问题就越少。
十六、定时器:Timer 的使用方式
Timer 可以理解为“延迟执行一次的线程”。
示例
import threading
def say_hello():
print("3 秒后执行了这个函数")
timer = threading.Timer(3, say_hello)
timer.start()
取消定时任务
timer.cancel()
适合场景
- 延迟执行某个函数
- 超时控制
- 定时提醒
- 简单一次性触发任务
注意:
Timer本质上还是线程,不适合拿来做复杂调度系统。
十七、守护线程:daemon=True 到底意味着什么?
很多人看到 daemon=True,大概知道“主线程结束它也会结束”,但理解不够深。
1. 什么是守护线程
守护线程的含义是:
当程序中只剩守护线程时,整个进程会直接退出。
也就是说,守护线程不会阻止程序结束。
2. 示例
import threading
import time
def worker():
while True:
print("后台线程运行中...")
time.sleep(1)
t = threading.Thread(target=worker, daemon=True)
t.start()
time.sleep(3)
print("主线程结束")
运行后你会看到:
- 后台线程打印几次
- 主线程结束后,整个程序直接退出
- 守护线程不会继续保活
3. 什么时候适合用守护线程
适合:
- 后台日志刷新
- 心跳检测
- 非关键监控线程
- 程序退出时可直接丢弃的任务
不适合:
- 必须执行完的写库、落盘、转账、关键业务逻辑
一个实战原则
只把“可以随时中断也无所谓”的线程设为守护线程。
十八、GIL 是什么?为什么它总和多线程一起出现?
这是 Python 多线程绕不过去的话题。
1. GIL 全称
GIL = Global Interpreter Lock
中文常叫:全局解释器锁
2. 它做了什么
在 CPython 解释器中,GIL 保证:
同一时刻只有一个线程执行 Python 字节码。
这意味着即使你开了多个线程,它们也不是同时执行 Python 代码,而是轮流拿到 GIL 去执行。
3. 那为什么 IO 密集型任务仍然有效?
因为线程在等待 IO 时,会释放 GIL。
例如:
- 网络请求等待响应
sleep()- 文件读写等待磁盘
- 某些 C 扩展内部阻塞调用
线程 A 等待 IO 时,线程 B 就有机会运行。
所以对于 IO 型任务,多线程依然能提升整体吞吐。
4. 为什么 CPU 密集型任务不适合?
因为 CPU 密集任务一直在执行 Python 计算逻辑,线程会频繁竞争 GIL。
结果可能是:
- 不能真正并行
- 上下文切换开销变大
- 性能不升反降
5. 一张表彻底记住
| 任务类型 | 使用 threading 效果 |
|---|---|
| 网络请求 | 很适合 |
| 文件 IO | 很适合 |
| 数据库访问 | 很适合 |
| 批量接口调用 | 很适合 |
| 图像纯计算 | 不太适合 |
| 大规模数值运算 | 不太适合 |
| CPU 密集循环 | 通常不适合 |
十九、threading 适合做什么,不适合做什么
适合的场景 ✅
- 爬虫抓取多个页面
- 多接口并发请求
- 批量处理文件
- 日志/埋点异步写入
- 消费队列任务
- GUI 程序后台执行任务
- 需要共享内存、协作方便的轻量并发任务
不适合的场景 ❌
- 大规模科学计算
- 视频编码
- 图像处理中的纯 CPU 计算
- 复杂并行计算任务
- 想依靠多线程“榨满多核 CPU”的场景
这种情况该怎么办?
- CPU 密集:优先考虑
multiprocessing - 高并发网络:优先考虑
asyncio - 简化线程池管理:考虑
concurrent.futures.ThreadPoolExecutor
二十、实战案例一:多线程批量下载网页内容
先来一个非常典型的 IO 密集型案例。
串行版本
import requests
import time
urls = [
"https://example.com",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
]
start = time.time()
for url in urls:
response = requests.get(url, timeout=5)
print(url, response.status_code)
print("总耗时:", time.time() - start)
如果每个请求都要等 1 秒,总耗时会比较明显。
多线程版本
import threading
import requests
import time
urls = [
"https://example.com",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
]
results = []
lock = threading.Lock()
def fetch(url):
try:
response = requests.get(url, timeout=5)
with lock:
results.append((url, response.status_code, len(response.text)))
except Exception as e:
with lock:
results.append((url, "ERROR", str(e)))
threads = []
start = time.time()
for url in urls:
t = threading.Thread(target=fetch, args=(url,))
threads.append(t)
t.start()
for t in threads:
t.join()
for item in results:
print(item)
print("总耗时:", time.time() - start)
为什么这里多线程更有效?
因为每个线程大部分时间都在等待网络返回,而不是一直占用 CPU。
二十一、实战案例二:生产者-消费者模型
这是线程编程里最经典的模型之一。
业务场景
- 生产者负责生成任务
- 消费者负责处理任务
- 中间通过队列传递数据
示例代码
import threading
import queue
import time
import random
task_queue = queue.Queue()
def producer():
for i in range(10):
item = f"task-{i}"
print(f"[生产者] 生产 {item}")
task_queue.put(item)
time.sleep(random.uniform(0.1, 0.5))
# 通知消费者结束
for _ in range(3):
task_queue.put(None)
def consumer(worker_id):
while True:
item = task_queue.get()
if item is None:
print(f"[消费者-{worker_id}] 收到结束信号")
task_queue.task_done()
break
print(f"[消费者-{worker_id}] 正在处理 {item}")
time.sleep(random.uniform(0.5, 1.5))
print(f"[消费者-{worker_id}] 完成 {item}")
task_queue.task_done()
producer_thread = threading.Thread(target=producer)
consumer_threads = [
threading.Thread(target=consumer, args=(i,))
for i in range(3)
]
producer_thread.start()
for t in consumer_threads:
t.start()
producer_thread.join()
task_queue.join()
for t in consumer_threads:
t.join()
print("所有任务处理完成")
这个模型为什么好?
因为它有几个很强的工程优势:
- 生产和消费解耦
- 可以平滑削峰
- 队列天然线程安全
- 容易扩展消费者数量
- 逻辑清晰,可维护性高
这也是生产环境里极常见的一种模式。
二十二、实战案例三:限制最大并发数的爬虫/接口调用器
很多接口都有频率限制,或者你不希望一口气发太多请求。这时候 Semaphore 特别适合。
import threading
import time
import random
semaphore = threading.Semaphore(3)
def call_api(task_id):
with semaphore:
print(f"任务 {task_id} 开始请求")
time.sleep(random.uniform(1, 2))
print(f"任务 {task_id} 请求结束")
threads = []
for i in range(10):
t = threading.Thread(target=call_api, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
这个方案的价值
- 线程数可以多
- 但真正进入核心执行区的并发数被严格限制
- 很适合第三方接口调用、下载器、数据库连接池外层控制
二十三、常见坑位与排查思路
这部分非常重要。
真正拉开水平差距的,往往不是“会写线程”,而是“出了问题能不能快速定位”。
1. 忘记 join(),主线程提前结束
t.start()
print("主线程结束")
如果不 join(),主线程可能直接退出,导致你误以为线程“没执行完”。
排查思路
- 检查是否等待了所有子线程
- 关键流程不要依赖“线程自己跑完”
2. 把 run() 当成 start() 用
这是新手高频错误。
t.run()
这不是启动线程,而是普通方法调用。
排查思路
- 看是否真的调用了
start() - 打印
threading.current_thread().name验证线程身份
3. 共享变量未加锁
这类 bug 最烦,因为它常常“偶现”。
排查信号
- 结果数量偶尔不对
- 计数器结果偏小
- 列表长度异常
- 状态偶发错乱
解决思路
- 对共享可变状态加锁
- 或改成消息传递(
Queue) - 尽量减少共享写操作
4. 死锁
死锁常见于:
- 多把锁交叉获取
- 忘记释放锁
- 同一线程重复申请普通
Lock - 条件等待与通知逻辑不匹配
经典死锁示例
# 线程 A
with lock1:
with lock2:
pass
# 线程 B
with lock2:
with lock1:
pass
如果两个线程同时执行,就可能互相等待。
避免死锁的经验
✅ 锁顺序保持一致
✅ 尽量减少锁嵌套
✅ 用 with 自动释放
✅ 能用 Queue 就少用共享锁
✅ 复杂场景给锁获取加超时
5. 守护线程误用
你把关键任务放进守护线程,主线程一结束,任务就直接没了。
典型事故
- 日志没刷完
- 文件没写完整
- 数据库事务没提交
- 消息没发出去
经验
关键任务线程,不要设成守护线程。
6. 在线程里异常静默丢失
线程里的异常不会总是像主线程那样明显暴露。
建议写法
import threading
import traceback
def worker():
try:
1 / 0
except Exception:
print(f"{threading.current_thread().name} 发生异常")
traceback.print_exc()
更好的工程实践
- 在线程函数最外层统一捕获异常
- 记录日志
- 把异常信息存到结果队列
- 主线程统一收集并处理
7. 线程数量开太多
并不是线程越多越好。
线程过多可能导致:
- 内存上涨
- 调度开销增大
- 上下文切换频繁
- 资源被打爆
- 接口方把你限流
一个常见误区
“我有 1000 个任务,就开 1000 个线程。”
这通常不是好主意。
更合理的方式是:
- 固定数量线程池
Queue投递任务- 或者
Semaphore控制并发
二十四、threading 和 concurrent.futures 怎么选?
这个问题非常常见。
1. threading
更底层、更灵活,适合:
- 你要自己管理线程生命周期
- 你要用锁、事件、条件变量等同步原语
- 你要搭建复杂线程协作逻辑
2. ThreadPoolExecutor
更高级、更省心,适合:
- 一批独立任务并发执行
- 不想手动管理线程对象
- 想方便地拿返回值、异常、超时结果
对比表
| 对比项 | threading | ThreadPoolExecutor |
|---|---|---|
| 灵活性 | 高 | 中 |
| 上手难度 | 较高 | 低 |
| 适合复杂同步 | 是 | 一般 |
| 适合任务池 | 一般 | 很适合 |
| 返回值处理 | 自己做 | 内置 Future |
经验建议
- 复杂线程协作:用
threading - 简单并发跑任务:优先
ThreadPoolExecutor
二十五、面试高频问题总结
下面这部分,几乎可以当成面试速记卡来背。
1. Python 多线程能并行吗?
答:
- 对于 IO 密集型任务,能提升并发效率
- 对于 CPU 密集型任务,在 CPython 下由于 GIL,通常不能真正发挥多核并行优势
2. start() 和 run() 的区别是什么?
答:
start()会启动新线程,并自动调用run()run()只是普通方法,直接调用不会创建新线程
3. join() 的作用是什么?
答:
- 让当前线程等待目标线程执行结束
4. 什么是线程安全?
答:
- 多个线程同时访问共享资源时,程序结果仍然正确、稳定、可预期
5. Lock 和 RLock 的区别?
答:
Lock不能被同一线程重复获取RLock可以被同一线程重复获取,适合嵌套调用场景
6. Condition、Event、Semaphore 分别适合什么?
答:
Condition:等待某个条件成立并通知Event:简单线程通知/广播Semaphore:限制同时访问资源的线程数量
7. 为什么 Queue 在线程通信中更推荐?
答:
- 它是线程安全的
- 能减少共享变量带来的竞态问题
- 更符合生产者-消费者模型
8. 什么是守护线程?
答:
- 守护线程不会阻止进程退出;当只剩守护线程时,进程会直接结束
二十六、一份可落地的最佳实践清单
这部分我建议你收藏起来,写多线程代码时拿出来对照。
多线程开发建议清单
设计层面
- ✅ 优先判断任务是不是 IO 密集型
- ✅ 能不用共享状态就不用共享状态
- ✅ 优先使用
Queue做线程间通信 - ✅ 优先限制并发度,而不是无脑开线程
编码层面
- ✅ 用
start()启动线程,不要直接调run() - ✅ 用
join()等待关键线程结束 - ✅ 用
with lock:代替手动acquire/release - ✅ 把加锁范围控制到最小
- ✅ 线程入口统一捕获异常
架构层面
- ✅ 简单任务并发,优先考虑线程池
- ✅ CPU 密集任务考虑
multiprocessing - ✅ 高并发网络任务考虑
asyncio - ✅ 对外部资源访问要有并发上限控制
一张总表:threading 常用工具怎么选
| 工具 | 作用 | 适用场景 |
|---|---|---|
Thread | 创建线程 | 基础线程任务 |
Lock | 互斥访问 | 保护共享数据 |
RLock | 可重入互斥 | 嵌套加锁 |
Condition | 条件等待/通知 | 等状态变化 |
Event | 信号通知 | 开始/停止广播 |
Semaphore | 限制并发数 | 下载器、接口限流 |
Queue | 线程安全通信 | 生产者-消费者 |
Timer | 延迟执行 | 简单定时任务 |
二十七、总结
threading 真正难的,不是 API 本身,而是并发思维。
你会发现它的学习过程大概分三个阶段:
第一阶段:会用
知道怎么:
- 创建线程
- 启动线程
- 等待线程结束
第二阶段:能写
知道怎么:
- 用锁保护共享数据
- 用事件、条件变量做线程协作
- 用队列做线程间通信
第三阶段:能写对
知道怎么:
- 分辨任务是否适合多线程
- 避开 GIL 误区
- 控制线程数量
- 排查竞态、死锁、异常丢失等问题
- 写出更稳定、更工程化的并发代码
最后送你一句很实用的话:
Python 多线程的重点,从来不是“把程序改成多线程”,而是“把适合并发的那部分,安全地并发起来”。
这才是 threading 真正的价值。
附:一个简洁但实用的多线程模板
你在实际开发中,经常可以从下面这个模板起步。
import threading
import queue
import traceback
task_queue = queue.Queue()
result_queue = queue.Queue()
def worker():
while True:
item = task_queue.get()
if item is None:
task_queue.task_done()
break
try:
# 处理任务
result = item * 2
result_queue.put((item, result, None))
except Exception as e:
result_queue.put((item, None, traceback.format_exc()))
finally:
task_queue.task_done()
# 创建固定数量工作线程
threads = []
for _ in range(4):
t = threading.Thread(target=worker)
t.start()
threads.append(t)
# 投递任务
for i in range(10):
task_queue.put(i)
# 发送结束信号
for _ in threads:
task_queue.put(None)
# 等待任务完成
task_queue.join()
# 等待线程结束
for t in threads:
t.join()
# 获取结果
while not result_queue.empty():
item, result, err = result_queue.get()
if err:
print(f"任务 {item} 失败:\n{err}")
else:
print(f"任务 {item} 成功,结果={result}")
这个模板的优点是:
- 线程数可控
- 任务通信安全
- 支持错误收集
- 容易扩展成生产代码
更多推荐




所有评论(0)