多线程
从 threading.Thread 原语到 ThreadPoolExecutor 的完整路径。多线程是 Python 最常用的并发方案,简单、好用、大多数 I/O 密集场景够用。
1. threading.Thread 基本用法
1.1 最简单的线程
import threading
def worker(n):
print(f"线程 {n} 开始")
# ... 执行任务 ...
print(f"线程 {n} 结束")
# 创建线程
threads = [threading.Thread(target=worker, args=(i,)) for i in range(5)]
# 启动(不是 run!)
for t in threads:
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print("全部完成")
1.2 守护线程(daemon)
t = threading.Thread(target=background_task, daemon=True)
t.start()
# 主线程结束时,daemon 线程会被自动终止
守护线程适合后台任务(心跳检测、日志刷盘),主线程退出时不需要等它。
1.3 join() 的作用
t.start()
t.join() # 阻塞当前线程,直到 t 执行完毕
print("t 已完成")
不调用 join(),主线程不会等子线程——程序可能在子线程还没跑完时就退出了。
2. 竞态条件:为什么需要锁
2.1 一个计数器的例子
import threading
counter = 0
def increment():
global counter
for _ in range(100_000):
counter += 1 # 这一行不是原子操作!
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(counter)
# 期望 1_000_000,实际可能是 283_457、412_891... 每次都不同
2.2 为什么会这样
counter += 1 看起来是一行代码,实际分三步:
- 读取 counter 的当前值
- 加 1
- 写回 counter
两个线程可能同时读到同一个值,各自加 1 后写回——结果只加了 1,而不是 2。这就是竞态条件(Race Condition)。
2.3 用 Lock 修复
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100_000):
with lock: # 自动获取和释放锁
counter += 1 # 临界区:同一时刻只有一个线程执行
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(counter) # 1_000_000(加锁保证正确性)
with lock 是最推荐的写法——自动获取锁,执行完自动释放,即使出异常也不会忘记释放。
3. 同步原语速览
Python 的 threading 模块提供了多种同步工具:
| 原语 | 说明 | 典型场景 |
|---|---|---|
Lock | 互斥锁,同一时刻只有一个线程持有 | 保护共享变量 |
RLock | 可重入锁,同一线程可多次获取 | 嵌套锁场景(防止死锁) |
Semaphore | 信号量,控制同时访问的线程数量 | 限制并发数(如限速器) |
Event | 线程间事件通知 | 一个线程通知,多个线程等待 |
Condition | 条件变量,支持 wait/notify | 生产者-消费者模式 |
Barrier | 栅栏,所有线程到齐后同时继续 | 多阶段并行计算 |
常用示例
import threading
import time
# --- Semaphore:限制最多 3 个线程同时执行 ---
semaphore = threading.Semaphore(3)
def limited_task(n):
with semaphore:
print(f"任务 {n} 执行中")
time.sleep(1)
# --- Event:线程间通知 ---
event = threading.Event()
def producer():
time.sleep(1)
print("数据就绪")
event.set() # 通知消费者
def consumer():
event.wait() # 阻塞直到 event 被 set
print("开始处理")
4. ThreadPoolExecutor 重点讲解
concurrent.futures.ThreadPoolExecutor 是现代 Python 多线程的首选 API。
4.1 executor.map vs submit + as_completed
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def fetch(url):
time.sleep(1)
return f"result of {url}"
urls = ["https://a.com", "https://b.com", "https://c.com"]
方式一:map —— 简单,保持输入顺序
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(fetch, urls))
# results 按 urls 的顺序返回,不管哪个先完成
# results = ['result of https://a.com', 'result of https://b.com', ...]
方式二:submit + as_completed —— 灵活,按完成顺序
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(fetch, url): url for url in urls}
for future in as_completed(futures):
url = futures[future]
try:
result = future.result()
print(f"{url}: {result}")
except Exception as e:
print(f"{url} 失败: {e}")
| 对比 | map | submit + as_completed |
|---|---|---|
| 结果顺序 | 按输入顺序 | 按完成顺序 |
| 错误处理 | 第一个异常就抛出 | 可以逐个捕获 |
| 灵活性 | 低(固定模式) | 高(可以做更复杂的逻辑) |
| 适用场景 | 简单的批量处理 | 需要"先完成先处理"或错误隔离 |
4.2 future 对象
submit() 返回一个 Future 对象,代表一个异步操作的结果:
future = executor.submit(fetch, url)
# 获取结果(阻塞直到完成)
result = future.result()
# 获取结果,带超时
result = future.result(timeout=10)
# 检查是否完成
future.done() # True / False
# 获取异常(如果任务抛了异常)
future.exception() # Exception 对象,或 None
# 取消任务(如果还没开始执行)
future.cancel() # True / False
4.3 max_workers 怎么选
| 场景 | 建议值 | 原因 |
|---|---|---|
| I/O 密集(网络请求) | 10-100 | 线程大部分时间在等,多开无妨 |
| I/O 密集(文件读写) | 10-50 | 受磁盘 I/O 限制 |
| CPU 密集 | CPU 核心数 | 但建议用多进程代替 |
5. 实战模式:限速器(Rate Limiter)
API 通常有频率限制(如每秒 10 次请求)。用 Semaphore 可以实现简单的限速:
import threading
import time
from concurrent.futures import ThreadPoolExecutor
# 信号量限制同时最多 5 个请求
semaphore = threading.Semaphore(5)
# 额外的锁控制每秒请求速率
rate_lock = threading.Lock()
request_count = 0
def rate_limited_fetch(url):
"""带限速的 HTTP 请求。"""
global request_count
with semaphore: # 限制并发数
with rate_lock:
request_count += 1
if request_count >= 10:
print("达到速率上限,等待...")
time.sleep(1)
request_count = 0
# 实际的请求逻辑
time.sleep(0.1) # 模拟 HTTP 请求
return f"OK: {url}"
urls = [f"https://api.example.com/item/{i}" for i in range(20)]
with ThreadPoolExecutor(max_workers=5) as executor:
results = list(executor.map(rate_limited_fetch, urls))
print(f"完成 {len(results)} 个请求")
6. 线程安全注意事项
6.1 不是所有操作都线程安全
# 这些操作是原子的(CPython 实现保证):
# - counter += 1 (实际上是 .load + .add + .store,不原子!)
# - list.append(x) 是原子的
# - dict[key] = value 是原子的
# 但组合操作不是原子的:
# - if key not in d: d[key] = value ← 竞态条件
# - if not lst: lst.append(x) ← 竞态条件
6.2 用 Queue 代替共享变量
import queue
import threading
q = queue.Queue() # 线程安全的队列
def producer():
for i in range(10):
q.put(i)
def consumer():
while True:
item = q.get()
if item is None:
break
print(f"处理: {item}")
q.task_done()
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start(); t2.start()
t1.join(); q.put(None); t2.join()
queue.Queue 内部已处理好锁,是线程间传递数据的首选方式。
7. 小结
| 知识点 | 要记住的 |
|---|---|
Thread 基本用法 | start() 启动,join() 等待,daemon=True 守护 |
| 竞态条件 | 共享变量并发修改导致数据不一致 |
Lock | with lock: 保护临界区,永远用上下文管理器 |
ThreadPoolExecutor | 首选 API,map 适合简单场景,submit + as_completed 适合复杂场景 |
max_workers | I/O 密集可以设大,CPU 密集不超过核心数 |
queue.Queue | 线程间传数据的首选方式,内部已加锁 |
下一篇:python-multiprocessing,当 GIL 阻碍你时,多进程是 CPU 密集任务的解药。