Python 并发基础:threading/GIL 与 multiprocessing 的选型逻辑

文章目录

    • 一、并发与并行:两个概念的本质区别
      • [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_GLOBALLOAD_CONST 1BINARY_ADDSTORE_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。理解它们各自的边界,才能在真实场景中做出正确的技术选型------而不是拿着锤子找钉子。


如果觉得这篇文章有帮助,欢迎点赞、关注!

往期回顾:

相关推荐
m0_495496411 小时前
如何禁用 Vite 中的热更新(HMR)以避免 React 应用加载中断
jvm·数据库·python
shmily麻瓜小菜鸡1 小时前
在 VSCode 里遇到报红是因为 Angular 编译器无法识别
ide·vscode·angular.js
m0_741173331 小时前
MySQL中如何使用CAST实现类型转换_MySQL数据类型转换技巧
jvm·数据库·python
东北甜妹1 小时前
K8s -Daemonset,kube-proxy,service,statefulset
linux·运维·服务器
qq_413502021 小时前
如何用 bubbles 属性让自定义事件穿透多个 Web Components
jvm·数据库·python
FreeGo~1 小时前
手撕C++】内存管理:感受C++的魅力吧
开发语言·c++
m0_640309301 小时前
解决 Python 报错:ModuleNotFoundError: No module named ‘pkg_resources’
开发语言·python
地球资源数据云1 小时前
2015年中国30米分辨率沼泽湿地空间分布数据集
大数据·数据结构·数据库·人工智能·机器学习
郝学胜-神的一滴1 小时前
深度学习核心:损失函数完全解析 —— 从原理到 PyTorch 实战
人工智能·pytorch·python·深度学习·机器学习