文章目录
-
- 一、并发与并行:两个概念的本质区别
-
- [1.1 什么是并发(Concurrency)](#1.1 什么是并发(Concurrency))
- [1.2 什么是并行(Parallelism)](#1.2 什么是并行(Parallelism))
- [1.3 两者的核心区别](#1.3 两者的核心区别)
- [二、threading 模块:I/O 密集型任务的并发方案](#二、threading 模块:I/O 密集型任务的并发方案)
-
- [2.1 threading 适用场景](#2.1 threading 适用场景)
- [2.2 Thread 的生命周期](#2.2 Thread 的生命周期)
- [三、GIL:Python 并发绕不过的门槛](#三、GIL:Python 并发绕不过的门槛)
-
- [3.1 GIL 是什么](#3.1 GIL 是什么)
- [3.2 GIL 的释放时机](#3.2 GIL 的释放时机)
- [3.3 GIL 对 CPU 密集型任务的影响](#3.3 GIL 对 CPU 密集型任务的影响)
- [3.4 GIL 的替代方案](#3.4 GIL 的替代方案)
- [四、线程同步原语:Lock、RLock、Condition、Semaphore 与 Event](#四、线程同步原语:Lock、RLock、Condition、Semaphore 与 Event)
-
- [4.1 Lock:最基础的互斥锁](#4.1 Lock:最基础的互斥锁)
- [4.2 RLock:可重入锁](#4.2 RLock:可重入锁)
- [4.3 Condition:条件变量](#4.3 Condition:条件变量)
- [4.4 Semaphore:信号量](#4.4 Semaphore:信号量)
- [4.5 Event:线程间信号通知](#4.5 Event:线程间信号通知)
- [五、生产者-消费者:用 queue.Queue 实现线程安全队列](#五、生产者-消费者:用 queue.Queue 实现线程安全队列)
-
- [5.1 queue.Queue 的优势](#5.1 queue.Queue 的优势)
- [5.2 生产者-消费者队列工作流程](#5.2 生产者-消费者队列工作流程)
- [六、multiprocessing:绕过 GIL 的 CPU 并行方案](#六、multiprocessing:绕过 GIL 的 CPU 并行方案)
-
- [6.1 multiprocessing 的核心价值](#6.1 multiprocessing 的核心价值)
- [6.2 Pool:进程池并行](#6.2 Pool:进程池并行)
- [6.3 进程间通信:Queue、Pipe 与 shared_memory](#6.3 进程间通信:Queue、Pipe 与 shared_memory)
- [6.4 multiprocessing 与 threading 的内存模型对比](#6.4 multiprocessing 与 threading 的内存模型对比)
- [七、选型决策树:threading、multiprocessing 与 asyncio 如何选](#七、选型决策树:threading、multiprocessing 与 asyncio 如何选)
- 八、concurrent.futures:统一封装的简洁之美
-
- [8.1 线程池 vs 进程池的内部实现](#8.1 线程池 vs 进程池的内部实现)
- [8.2 concurrent.futures 完整示例](#8.2 concurrent.futures 完整示例)
- [九、asyncio 与前两者的关系:单线程内的并发](#九、asyncio 与前两者的关系:单线程内的并发)
-
- [9.1 asyncio 是什么](#9.1 asyncio 是什么)
- [9.2 asyncio 与 threading/multiprocessing 的关系](#9.2 asyncio 与 threading/multiprocessing 的关系)
- [9.3 asyncio + threading:混合使用场景](#9.3 asyncio + threading:混合使用场景)
- 十、总结与知识点串联
前置知识串联 :本文建立在文件操作、类与对象、模块与包、标准库精讲的基础之上。理解 GIL 的机制需要知道 Python 解释器是如何执行字节码的;理解 multiprocessing 的 shared_memory 需要知道 Python 对象的内存布局(见 内存管理)。
一、并发与并行:两个概念的本质区别
1.1 什么是并发(Concurrency)
并发 指的是同一时间段内 有多个任务在推进,但任意时刻只有一个任务在 CPU 上执行。从人的视角看,多个任务"同时"进行;从 CPU 的视角看,任务在交替执行。
日常生活的类比:一个人同时处理多封邮件------在等待邮件服务器响应的间隙里,切换去回复另一封邮件。单核 CPU 的"时间片轮转"本质上就是并发。
1.2 什么是并行(Parallelism)
并行 指的是同一时刻有多个任务真正同时在多个 CPU 核心上执行。这需要硬件支持------至少两个 CPU 核心。
日常生活的类比:两个人各自同时处理一叠邮件,互不干扰。
1.3 两者的核心区别
python
# 并发:交替执行,看起来"同时"------单核 CPU 也能实现
# 任务 A → [切换] → 任务 B → [切换] → 任务 A → ...
# 时序:|-----------时间----------->
# 并行:同时执行,真正同时------必须多核 CPU
# 核心1: [====任务 A====]
# 核心2: [====任务 B====]
# 时序:|-----------时间---------->
Python 中:
- threading 适合并发(I/O 等待期间切换)
- multiprocessing 适合并行(CPU 密集计算真正同时执行)
理解了这个区别,就能明白为什么"给 threading 加锁"解决不了 Python CPU 密集型任务的性能问题------锁只是让并发变成了串行,并不能产生并行。
二、threading 模块:I/O 密集型任务的并发方案
2.1 threading 适用场景
当任务大部分时间在等待 I/O(网络请求、文件读写、数据库查询)时,threading 非常高效------等待期间不需要 CPU,线程可以切换去处理其他任务。
典型场景:
- 同时请求多个 API 接口
- 并发读写多个文件
- 数据库批量操作的异步化
python
import threading
import time
def fetch_url(url: str, delay: float):
"""模拟网络请求"""
print(f"[线程 {threading.current_thread().name}] 开始请求: {url}")
time.sleep(delay) # 模拟 I/O 等待
print(f"[线程 {threading.current_thread().name}] 完成: {url}")
# 顺序执行:总耗时 = 1s + 2s + 3s = 6s
start = time.perf_counter()
fetch_url("api1.example.com", 1.0)
fetch_url("api2.example.com", 2.0)
fetch_url("api3.example.com", 3.0)
print(f"顺序执行耗时: {time.perf_counter() - start:.2f}s")
python
# 并发执行:总耗时 ≈ max(1s, 2s, 3s) = 3s
start = time.perf_counter()
threads = []
urls = [("api1.example.com", 1.0), ("api2.example.com", 2.0), ("api3.example.com", 3.0)]
for url, delay in urls:
t = threading.Thread(target=fetch_url, args=(url, delay))
t.start()
threads.append(t)
# 等待所有线程完成
for t in threads:
t.join()
print(f"并发执行耗时: {time.perf_counter() - start:.2f}s")
# 输出:并发执行耗时: 3.01s(而非 6s)
这就是 I/O 密集型任务用 threading 的核心价值:把等待的时间利用起来,让多个任务"交替等待"而不是"排队等待"。
2.2 Thread 的生命周期
每个 Thread 对象经历以下状态:
是,任务完成
是,遇到 I/O/锁等待
是,调用 join()
创建线程对象
Thread(target=func, args=...)
调用 start()
线程就绪,等待调度
被调度器选中
进入运行状态
(获得 GIL)
任务执行完毕?
或遇到 I/O 等待?
线程终止
资源回收
阻塞状态
(GIL 释放)
Thread 对象仍存在
但线程已终止
调用 start() 后
不可再调用
(⚠️ 常见错误)
几个关键点:
- 一个 Thread 对象只能调用一次
start()------重复调用会抛出RuntimeError。如果需要多次执行同一个任务,每次都创建新 Thread。 join()是阻塞等待------主线程调用t.join()会等子线程执行完毕才继续,用于确保所有任务完成。- daemon 线程 :创建时设置
threading.Thread(target=func, daemon=True),主线程退出时 daemon 线程会被强制终止,适合日志记录器、健康检查等"辅助性"任务。
python
import threading
# daemon 线程示例:后台心跳检测
def heartbeat():
while True: # 无限循环
print("心跳检测...")
threading.Event().wait(timeout=5)
t = threading.Thread(target=heartbeat, daemon=True)
t.start()
# 主线程退出时,daemon 线程自动终止
# 不需要手动 join 或停止信号
三、GIL:Python 并发绕不过的门槛
3.1 GIL 是什么
GIL(Global Interpreter Lock,全局解释器锁)是 CPython 实现中的一个机制:无论有多少个线程,同一时刻只有一个线程持有 GIL,其他线程只能等待。
这意味着:在 CPython 中,同一时刻只有一个线程在执行 Python 字节码。即使机器有 32 个 CPU 核心,Python 线程也无法真正并行执行 CPU 密集型代码。
GIL 不是 Python 的 bug,而是 CPython 的设计权衡------它简化了内存管理(所有线程共享同一个引用计数,无需额外同步),代价是 CPU 密集型任务无法利用多核。
3.2 GIL 的释放时机
GIL 并不是一直锁死,而是在以下情况自动释放:
python
# 触发 GIL 释放的操作(不完整列表)
- time.sleep() # 任何 I/O 等待(文件/网络/数据库)
- threading.Lock.acquire() # 等待锁时
- requests.get() # 网络 I/O
- open().read() # 文件 I/O
- 字节码执行 100 tick(默认)后强制切换
正是这个机制让 threading 对 I/O 密集型任务有效------I/O 等待期间 GIL 释放,其他线程可以执行。
3.3 GIL 对 CPU 密集型任务的影响
用 benchmark 直观感受 GIL 的影响:
python
import threading
import multiprocessing
import time
import math
def cpu_bound(n: int) -> float:
"""CPU 密集型:计算 1~n 的所有素数"""
count = 0
for i in range(2, n):
is_prime = True
for j in range(2, int(math.sqrt(i)) + 1):
if i % j == 0:
is_prime = False
break
if is_prime:
count += 1
return count
N = 200000
# 单线程基准
start = time.perf_counter()
cpu_bound(N)
print(f"单线程耗时: {time.perf_counter() - start:.2f}s")
# threading × 4(受 GIL 限制,几乎没有加速)
start = time.perf_counter()
threads = [threading.Thread(target=cpu_bound, args=(N,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"threading × 4 耗时: {time.perf_counter() - start:.2f}s")
# multiprocessing × 4(真正并行)
start = time.perf_counter()
with multiprocessing.Pool(4) as pool:
pool.map(cpu_bound, [N] * 4)
print(f"multiprocessing × 4 耗时: {time.perf_counter() - start:.2f}s")
典型输出:
单线程耗时: 12.34s
threading × 4 耗时: 12.89s ← 几乎一样,说明 GIL 阻止了并行
multiprocessing × 4 耗时: 3.45s ← 约 4 倍加速
这个结果说明:threading 对 CPU 密集型任务完全无效(甚至因线程切换开销略慢),只有 multiprocessing 才能真正利用多核。
3.4 GIL 的替代方案
如果不想用 multiprocessing,有几种绕过 GIL 的途径:
| 方案 | 说明 | 适用场景 |
|---|---|---|
| multiprocessing | 每个进程独立 GIL | CPU 密集型,需要多核并行 |
| C 扩展释放 GIL | NumPy、Cython 可在 C 代码中释放 GIL | 数值计算、图像处理 |
| 其他 Python 实现 | Jython(无 GIL)、PyPy(实验性) | 特定场景 |
| uvloop / asyncio | 单线程事件循环 | I/O 密集型(不需要多线程) |
NumPy 的矩阵运算能跑满所有 CPU 核心,正是因为它的核心计算在 C 层,C 层代码可以主动释放 GIL,不受 Python GIL 限制。
四、线程同步原语:Lock、RLock、Condition、Semaphore 与 Event
多线程程序最怕竞态条件(Race Condition)------多个线程同时访问和修改共享资源,导致结果不确定。
python
# ⚠️ 竞态条件示例:两个线程同时 +1,结果不对
counter = 0
def increment():
global counter
for _ in range(1000000):
counter += 1 # ⚠️ 这一行不是原子操作!
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start(); t2.start()
t1.join(); t2.join()
print(counter) # 期望 2000000,实际通常更少------部分更新被覆盖了
counter += 1 在字节码层面是三条指令:LOAD_GLOBAL → LOAD_CONST 1 → BINARY_ADD → STORE_GLOBAL。如果线程 A 在执行到一半时被线程 B 打断,线程 B 的结果就会覆盖线程 A 的结果。
解决竞态条件,需要线程同步原语。
4.1 Lock:最基础的互斥锁
python
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(1000000):
with lock: # 获取锁,执行完代码块后自动释放
counter += 1 # 现在是安全的了
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start(); t2.start()
t1.join(); t2.join()
print(counter) # 正确输出:2000000
with lock: 是最推荐的写法------确保锁一定会被释放(即使代码抛异常),避免死锁。
4.2 RLock:可重入锁
普通 Lock 在同一个线程内不能重复 acquire,否则会死锁:
python
# ⚠️ 普通 Lock 会死锁
lock = threading.Lock()
def outer():
with lock:
inner() # 尝试再次获取锁------死锁!
def inner():
with lock: # 等待 outer 释放锁,但 outer 在等 inner 返回
pass
# RLock 可以解决:同一线程可以多次 acquire
rlock = threading.RLock()
def outer():
with rlock:
inner() # RLock 允许同一线程重入
def inner():
with rlock: # 可以成功获取,因为持有者还是当前线程
pass
**RLock(可重入锁)**允许同一线程多次 acquire,只有 release 次数等于 acquire 次数时才真正释放。适合递归函数或嵌套调用场景。
4.3 Condition:条件变量
Condition(条件变量)用于等待某个条件成立的场景,常与 while 循环配合:
python
import threading
class BoundedBuffer:
"""有界缓冲区,生产者-消费者问题"""
def __init__(self, capacity=10):
self.capacity = capacity
self.buffer = []
self.lock = threading.Lock()
self.not_full = threading.Condition(self.lock) # 缓冲区未满
self.not_empty = threading.Condition(self.lock) # 缓冲区非空
def produce(self, item):
with self.not_full:
while len(self.buffer) == self.capacity:
self.not_full.wait() # 等待缓冲区有空间
self.buffer.append(item)
self.not_empty.notify() # 通知消费者有数据
def consume(self):
with self.not_empty:
while len(self.buffer) == 0:
self.not_empty.wait() # 等待生产者有数据
item = self.buffer.pop(0)
self.not_full.notify() # 通知生产者有空间
return item
wait() 会释放锁并阻塞,直到另一个线程调用 notify()/notify_all()。必须用 while 循环而非 if 判断,因为 wait 返回时可能有其他线程已经改变了条件。
4.4 Semaphore:信号量
Semaphore 维护一个计数器,控制同时访问某个资源的线程数量:
python
import threading
import time
# 限制同时只有 3 个连接
connections = threading.Semaphore(3)
def handle_request(request_id: int):
with connections:
print(f"[请求 {request_id}] 开始处理(当前活跃: {3 - connections._value} 个)")
time.sleep(1)
print(f"[请求 {request_id}] 完成")
# 10 个请求并发,但同时只有 3 个在执行
threads = [threading.Thread(target=handle_request, args=(i,)) for i in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
# 总耗时 ≈ 4s(3+3+3+1),而非 10s
Semaphore 常用于连接池、线程池等资源池场景。
4.5 Event:线程间信号通知
Event 用于一个线程通知另一个线程"某件事发生了":
python
import threading
import time
# 服务启动信号
service_ready = threading.Event()
def service():
print("服务初始化中...")
time.sleep(2) # 模拟初始化
service_ready.set() # 通知主线程:服务已就绪
print("服务已启动!")
def client():
print("等待服务就绪...")
service_ready.wait() # 阻塞,直到 set() 被调用
print("服务已就绪,开始发送请求!")
t1 = threading.Thread(target=service)
t2 = threading.Thread(target=client)
t1.start()
t2.start()
t1.join()
t2.join()
Event 的三个核心方法:
wait(timeout=None):阻塞等待,直到 flag 被设置或超时set():设置 flag(通知所有等待的线程)clear():清除 flag(重置状态)
五、生产者-消费者:用 queue.Queue 实现线程安全队列
5.1 queue.Queue 的优势
手动用 Lock + Condition 实现生产者-消费者容易出错(上面的 BoundedBuffer 代码有 40 行)。Python 标准库的 queue.Queue 已经封装好了一切:
python
import threading
import queue
import time
import random
def producer(q: queue.Queue, item_count: int):
"""生产者:生成任务并放入队列"""
for i in range(item_count):
item = {"id": i, "data": random.randint(1, 100)}
q.put(item) # 队列满时自动阻塞
print(f"[生产者] 已放入任务 {i}")
def consumer(q: queue.Queue, name: str):
"""消费者:从队列取任务并处理"""
while True:
item = q.get() # 队列空时自动阻塞
if item is None: # 哨兵值:通知消费者结束
q.task_done()
break
print(f"[消费者 {name}] 处理任务 {item['id']},结果: {item['data'] ** 2}")
q.task_done() # 标记任务完成
# 创建队列(最大容量 20)
task_queue = queue.Queue(maxsize=20)
# 启动 3 个消费者
consumers = [threading.Thread(target=consumer, args=(task_queue, f"C{i}")) for i in range(3)]
# 启动生产者
producer_thread = threading.Thread(target=producer, args=(task_queue, 30))
producer_thread.start()
# 启动消费者
for c in consumers:
c.start()
# 生产者结束后,放入 3 个哨兵值(每个消费者一个)
producer_thread.join()
for _ in range(3):
task_queue.put(None) # 哨兵值
# 等待所有任务完成
for c in consumers:
c.join()
print("所有任务处理完成!")
queue.Queue 的几个特点:
- 线程安全 :内部已实现所有必要的锁,
put()/get()都是原子操作 - 阻塞机制 :
put()在队列满时自动阻塞,get()在队列空时自动阻塞 task_done()+join():优雅地等待所有任务被消费完毕
5.2 生产者-消费者队列工作流程
消费者侧(3 个线程)
queue.Queue(线程安全)
生产者侧
生成任务
(item)
put(item)
队列加入
任务队列
最多 20 项
Consumer 1
get()
取出任务
Consumer 2
get()
取出任务
Consumer 3
get()
取出任务
处理任务
处理任务
处理任务
task_done()
task_done()
task_done()
主线程 join()
等待所有 task_done
任务完成
六、multiprocessing:绕过 GIL 的 CPU 并行方案
6.1 multiprocessing 的核心价值
multiprocessing 通过进程而非线程实现并行。每个进程有独立的 Python 解释器、独立的 GIL------这意味着可以真正利用多核 CPU。
python
import multiprocessing
import time
import math
def cpu_bound(n: int) -> float:
count = 0
for i in range(2, n):
is_prime = True
for j in range(2, int(math.sqrt(i)) + 1):
if i % j == 0:
is_prime = False
break
if is_prime:
count += 1
return count
N = 200000
# 单进程基准
start = time.perf_counter()
cpu_bound(N)
print(f"单进程: {time.perf_counter() - start:.2f}s")
# multiprocessing × 4(真正并行)
start = time.perf_counter()
with multiprocessing.Pool(4) as pool:
results = pool.map(cpu_bound, [N] * 4)
print(f"4 进程: {time.perf_counter() - start:.2f}s, 结果: {results}")
6.2 Pool:进程池并行
multiprocessing.Pool 是最常用的进程池接口:
python
from multiprocessing import Pool
# map:阻塞式并行映射,结果顺序与输入一致
with Pool(4) as pool:
results = pool.map(pow, [2] * 10, [i for i in range(10)])
# 等价于 [pow(2, 0), pow(2, 1), ..., pow(2, 9)]
# imap_unordered:非阻塞,返回迭代器,适合处理大量任务
with Pool(4) as pool:
for result in pool.imap_unordered(heavy_task, range(100)):
print(f"完成: {result}") # 按完成顺序输出,不保证顺序
# apply_async:异步提交单个任务
with Pool(4) as pool:
async_result = pool.apply_async(heavy_task, (42,))
result = async_result.get(timeout=10) # 获取结果,可设置超时
6.3 进程间通信:Queue、Pipe 与 shared_memory
multiprocessing 的进程不像线程那样共享内存------每个进程有独立的地址空间。如果需要在进程间传递数据,需要使用 IPC 机制:
python
from multiprocessing import Process, Queue, Pipe, Array, Value
import time
# 方式一:Queue(线程安全的进程间队列)
def producer_q(q: Queue):
for i in range(5):
q.put(i)
print(f"[生产者] 放入 {i}")
def consumer_q(q: Queue):
while True:
item = q.get(timeout=3) # 超时 3 秒后抛出异常
print(f"[消费者] 取出 {item}")
if item == 4:
break
q = Queue()
Process(target=producer_q, args=(q,)).start()
Process(target=consumer_q, args=(q,)).start().join()
# 方式二:shared_memory(Python 3.8+,高效共享内存)
# 适合需要共享大量数据的场景(如 NumPy 数组)
from multiprocessing import shared_memory
import numpy as np
def worker_shm(shm_name: str, shape: tuple, dtype: type):
"""从共享内存读取并处理数据"""
shm = shared_memory.SharedMemory(name=shm_name)
arr = np.ndarray(shape, dtype=dtype, buffer=shm.buf)
print(f"处理数据: {arr.sum()}")
shm.close()
# 创建共享内存
existing_shm = shared_memory.SharedMemory(create=True, size=1000, name="my_shm")
arr = np.ndarray((125,), dtype=np.float64, buffer=existing_shm.buf)
arr[:] = np.random.random(125)
# 启动工作进程
p = Process(target=worker_shm, args=("my_shm", (125,), np.float64))
p.start()
p.join()
# 清理
existing_shm.close()
existing_shm.unlink() # 删除共享内存段
6.4 multiprocessing 与 threading 的内存模型对比
multiprocessing(独立内存)
进程 1
独立 GIL
独立解释器
主内存
(每个进程独立地址空间)
进程 2
独立 GIL
独立解释器
主内存
(独立地址空间)
进程 3
独立 GIL
独立解释器
主内存
(独立地址空间)
threading(共享内存)
主内存(共享)
所有线程共享同一地址空间
线程 1
(GIL持有者)
线程 2
(等待GIL)
线程 3
(等待GIL)
threading 的优势是内存共享 (无需序列化/反序列化),但受 GIL 限制;multiprocessing 的优势是真正并行,但进程间通信需要序列化(pickle),有额外开销。
七、选型决策树:threading、multiprocessing 与 asyncio 如何选
这是 Python 并发最核心的问题。选错方案,轻则性能没有提升,重则引入难以排查的死锁和竞态条件。
是
否,少量连接
(< 100)
是,大量连接
(> 1000)
否
CPU 密集型
(计算/加密/ML)
是,混合使用
否,纯粹异步
是
否,数据量小
细粒度,大量小任务
粗粒度,独立任务
是
否
是
否,本机多核
任务类型判断
是否为
I/O 密集型?
(网络/文件/数据库等待多)
需要管理
大量并发连接?
✅ threading
简单直接,足够了
✅ asyncio
单线程事件循环
最高并发效率
✅ multiprocessing
真正多核并行
补充:threading 兼容
旧代码或同步库?
✅ asyncio + threading
asyncio 做调度
threading 处理同步库
✅ asyncio + aiohttp
全异步栈
补充:需要进程间
共享大量数据?
✅ multiprocessing
-
shared_memory
✅ threading -
queue.Queue
线程安全,无需序列化
补充:任务粒度?
✅ Pool.imap_unordered
自动负载均衡
✅ Pool.map
或独立 Process
补充:需要共享
NumPy 数组?
✅ multiprocessing
- shared_memory
numpy array 零拷贝共享
✅ 普通 Pool 即可
补充:需要跨机器
分布式?
✅ Celery / Ray
分布式任务队列
简化版快速对照:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 同时下载 10 个网页 | threading |
I/O 等待,GIL 释放 |
| 批量下载 1000 个网页 | asyncio + aiohttp |
大量并发连接,事件循环最优 |
| 批量计算 10000 个素数 | multiprocessing.Pool |
CPU 密集,GIL 限制 |
| 爬虫 + 数据处理混合 | asyncio + thread pool |
I/O 用协程,CPU 用线程池 |
| 数据库连接池(Web 服务) | asyncio + aiomysql |
高并发 I/O |
| 机器学习批量推理 | multiprocessing + shared_memory |
CPU 密集 + 大数组共享 |
八、concurrent.futures:统一封装的简洁之美
concurrent.futures 是 Python 3.2 引入的高层抽象,统一了线程池和进程池的 API,不需要关心底层是 threading 还是 multiprocessing:
python
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, as_completed
import time
def heavy_task(n: int) -> int:
"""计算 2^n"""
return 2 ** n
# 用 ThreadPoolExecutor(底层是 threading)
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(heavy_task, i): i for i in range(1, 11)}
for future in as_completed(futures):
print(f"任务 {futures[future]} 完成: {future.result()}")
# 换 ProcessPoolExecutor(底层是 multiprocessing)------API 完全一致!
with ProcessPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(heavy_task, i): i for i in range(1, 11)}
for future in as_completed(futures):
print(f"任务 {futures[future]} 完成: {future.result()}")
只需要改一个类名,就能从线程池切换到进程池------这是 concurrent.futures 最有用的地方:让"先写线程池,后面换成进程池"变得更简单。
8.1 线程池 vs 进程池的内部实现
ProcessPoolExecutor(进程池)
ThreadPoolExecutor(线程池)
任务队列
(threading.Queue)
线程 1
线程 2
线程 3
结果收集
任务队列
(multiprocessing.Queue)
进程 1
独立 GIL
进程 2
独立 GIL
进程 3
独立 GIL
结果收集
(Pickle 序列化)
调用方获取结果
关键差异:
- 线程池:共享内存,无需序列化(但受 GIL 限制)
- 进程池:独立内存,需要 pickle 序列化任务参数和返回值(不可 pickle 的对象无法通过进程池传递)
8.2 concurrent.futures 完整示例
python
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor, wait
import time
def fetch_data(api_id: int) -> dict:
"""模拟 API 请求"""
import random
time.sleep(random.uniform(0.1, 0.5))
return {"api_id": api_id, "status": "success", "data": [1, 2, 3]}
def process_data(data: dict) -> int:
"""模拟 CPU 计算"""
return sum(data["data"]) * data["api_id"]
# 阶段一:用线程池并发请求(I/O 密集)
print("阶段一:并发请求 API...")
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(fetch_data, i) for i in range(20)]
results = [f.result() for f in futures] # 等待所有请求完成
# 阶段二:用进程池并发计算(CPU 密集)
print("阶段二:并发处理数据...")
with ProcessPoolExecutor(max_workers=4) as executor:
processed = list(executor.map(process_data, results))
print(f"处理结果: {processed}")
混合使用 是生产环境的常见模式:I/O 阶段用线程池(或 asyncio),CPU 阶段用进程池。concurrent.futures 让这种混合使用变得非常自然。
九、asyncio 与前两者的关系:单线程内的并发
9.1 asyncio 是什么
asyncio 是协程(Coroutine) + 事件循环(Event Loop) 的组合,本质上是在单个线程内通过协作式调度实现并发。
核心概念:
- 协程(Coroutine) :用
async def定义的函数,调用时不会立即执行,而是返回一个协程对象 - 事件循环:单线程内的调度器,按顺序在协程之间切换(在 I/O 等待时切换)
await:主动让出控制权,等待另一个协程完成
python
import asyncio
async def fetch_url(url: str):
print(f"开始请求: {url}")
await asyncio.sleep(1) # 模拟异步 I/O
print(f"完成: {url}")
return f"数据 from {url}"
async def main():
# 并发执行 3 个协程(总耗时 ≈ 1s,而非 3s)
results = await asyncio.gather(
fetch_url("api1.example.com"),
fetch_url("api2.example.com"),
fetch_url("api3.example.com"),
)
print(results)
asyncio.run(main())
9.2 asyncio 与 threading/multiprocessing 的关系
是,CPU 密集型
(如 ML 推理、加密)
否,I/O 密集型
否,少量连接
或需要同步库
是,超高并发
或需要非阻塞
任务特征
是否需要
CPU 并行?
multiprocessing
真正多核并行
绕过 GIL
是否需要
高并发?
(> 1000 连接)
threading
简单直接
标准库支持好
asyncio
单线程事件循环
最高并发效率
结论
三者的本质关系:
单线程内
I/O 等待期间
协作切换
asyncio
单线程事件循环
零 GIL 竞争
多线程
I/O 等待期间
操作系统切换
threading
GIL 限制 CPU
但 I/O 时有效
多进程
真正同时
各自独立 GIL
multiprocessing
CPU 密集首选
无 GIL 竞争
9.3 asyncio + threading:混合使用场景
asyncio 并不是万能的------它无法处理同步库(如某些老旧数据库驱动)。此时可以在 asyncio 事件循环中运行一个线程池:
python
import asyncio
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=4)
async def main():
loop = asyncio.get_running_loop()
# 在线程池中运行同步阻塞代码(如同步数据库驱动)
def sync_db_query(sql: str):
# 这是同步代码,会阻塞线程------但线程不在 asyncio 主线程中
import time
time.sleep(1)
return f"查询结果: {sql}"
result = await loop.run_in_executor(executor, sync_db_query, "SELECT * FROM users")
print(result)
asyncio.run(main())
这种模式让 asyncio 处理高并发 I/O 调度,同时用线程池处理无法异步化的同步阻塞代码------两种方案的优势叠加。
十、总结与知识点串联
| 知识点 | 核心要点 | 对应前文 |
|---|---|---|
| 并发 vs 并行 | 同一时段 vs 同一时刻,GIL 影响的是 CPU 密集型的并行能力 | 内存管理 |
| threading 适用场景 | I/O 密集型(网络/文件),GIL 在 I/O 时自动释放 | 文件操作 |
| GIL 的限制 | CPU 密集型无法并行,threading 无加速效果 | 标准库 |
| 线程同步原语 | Lock/RLock/Condition/Semaphore/Event,防范竞态条件 | 类与对象 |
| queue.Queue | 线程安全队列,实现生产者-消费者模式 | 模块 #09 |
| multiprocessing | 进程独立 GIL,真正多核并行,进程间通信开销大 | 内存管理 |
| shared_memory | 进程间零拷贝共享 numpy 数组,适合大数据并行 | 标准库 |
| concurrent.futures | 统一线程池/进程池 API,改类名即可切换 | 模块 |
| asyncio | 单线程事件循环,高并发 I/O,不走 GIL 竞争 | 类与对象 |
Python 的并发三件套------threading、multiprocessing、asyncio------分别应对三种不同场景:threading 管 I/O 并发,multiprocessing 管 CPU 并行,asyncio 管高并发 I/O。理解它们各自的边界,才能在真实场景中做出正确的技术选型------而不是拿着锤子找钉子。
如果觉得这篇文章有帮助,欢迎点赞、关注!
往期回顾: