第 19 章 并发编程
19.1 GIL 的本质与影响
GIL(Global Interpreter Lock,全局解释器锁) 是 CPython 的一个互斥锁,确保同一时刻只有一个线程执行 Python 字节码。
# 即使创建多个线程,CPU 密集型任务也无法真正并行
import threading
import time
def cpu_bound(n):
total = 0
for i in range(n):
total += i * i
return total
start = time.perf_counter()
# 单线程
cpu_bound(10_000_000)
cpu_bound(10_000_000)
print(f"单线程: {time.perf_counter() - start:.2f}s")
start = time.perf_counter()
# 两个线程(不会更快,因为 GIL)
t1 = threading.Thread(target=cpu_bound, args=(10_000_000,))
t2 = threading.Thread(target=cpu_bound, args=(10_000_000,))
t1.start(); t2.start()
t1.join(); t2.join()
print(f"双线程: {time.perf_counter() - start:.2f}s")
# 双线程甚至可能比单线程更慢(线程切换开销)
GIL 的影响:
| 任务类型 | 多线程效果 | 推荐方案 |
|---|---|---|
| CPU 密集型 | 无加速(GIL 限制) | multiprocessing |
| I/O 密集型 | 有效加速 | threading 或 asyncio |
| 混合型 | 部分加速 | 视具体情况选择 |
GIL 在 I/O 等待时会释放,因此多线程对 I/O 密集型任务仍然有效。
19.2 多线程 threading
19.2.1 线程创建与管理
import threading
import time
# 方式 1:传入函数
def worker(name, seconds):
print(f"[{name}] 开始工作")
time.sleep(seconds)
print(f"[{name}] 完成")
t1 = threading.Thread(target=worker, args=("线程A", 2))
t2 = threading.Thread(target=worker, args=("线程B", 1))
t1.start()
t2.start()
t1.join() # 等待 t1 完成
t2.join() # 等待 t2 完成
print("所有线程完成")
# 方式 2:继承 Thread
class DownloadThread(threading.Thread):
def __init__(self, url):
super().__init__()
self.url = url
self.result = None
def run(self):
print(f"下载 {self.url}")
time.sleep(1) # 模拟下载
self.result = f"来自 {self.url} 的数据"
t = DownloadThread("https://example.com")
t.start()
t.join()
print(t.result)
守护线程
# 守护线程在主线程结束时自动终止
def background_task():
while True:
print("后台运行中...")
time.sleep(1)
t = threading.Thread(target=background_task, daemon=True)
t.start()
time.sleep(3)
print("主线程退出")
# 守护线程自动结束,不会阻止程序退出
19.2.2 锁和同步原语
import threading
# 无锁 — 竞态条件
counter = 0
def increment(n):
global counter
for _ in range(n):
counter += 1 # 不是原子操作!读 → 加 → 写
threads = [threading.Thread(target=increment, args=(100_000,)) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(f"期望: 1000000, 实际: {counter}") # 可能小于 1000000
# 用 Lock 解决
counter = 0
lock = threading.Lock()
def safe_increment(n):
global counter
for _ in range(n):
with lock: # 获取锁
counter += 1
threads = [threading.Thread(target=safe_increment, args=(100_000,)) for _ in range(10)]
for t in threads: t.start()
for t in threads: t.join()
print(f"结果: {counter}") # 精确 1000000
其他同步原语
import threading
# RLock — 可重入锁(同一线程可以多次获取)
rlock = threading.RLock()
with rlock:
with rlock: # 不会死锁
pass
# Semaphore — 信号量(限制并发数)
sem = threading.Semaphore(3) # 最多 3 个线程同时进入
def limited_task(name):
with sem:
print(f"{name} 获得许可")
time.sleep(1)
# Event — 线程间信号
event = threading.Event()
def waiter():
print("等待信号...")
event.wait() # 阻塞直到 event 被 set
print("收到信号!")
def setter():
time.sleep(2)
print("发送信号")
event.set()
# Condition — 条件变量
condition = threading.Condition()
items = []
def producer():
with condition:
items.append("item")
condition.notify() # 通知等待的消费者
def consumer():
with condition:
while not items:
condition.wait() # 释放锁并等待通知
item = items.pop()
print(f"消费: {item}")
19.2.3 线程安全
import threading
from queue import Queue
# Queue 是线程安全的 — 推荐用于线程间通信
def producer(q: Queue):
for i in range(5):
q.put(f"任务-{i}")
time.sleep(0.1)
q.put(None) # 结束信号
def consumer(q: Queue):
while True:
item = q.get() # 阻塞等待
if item is None:
break
print(f"处理: {item}")
q.task_done()
q = Queue(maxsize=10)
threading.Thread(target=producer, args=(q,)).start()
threading.Thread(target=consumer, args=(q,)).start()
# threading.local() — 线程本地存储
local_data = threading.local()
def worker(name):
local_data.name = name # 每个线程有独立的副本
time.sleep(0.1)
print(f"线程 {threading.current_thread().name}: {local_data.name}")
t1 = threading.Thread(target=worker, args=("Alice",))
t2 = threading.Thread(target=worker, args=("Bob",))
t1.start(); t2.start()
t1.join(); t2.join()
# 不会互相干扰
19.3 多进程 multiprocessing
多进程绕过 GIL,实现真正的并行计算:
19.3.1 进程创建与进程池
import multiprocessing
import time
import os
def cpu_bound(n):
print(f"进程 {os.getpid()} 开始计算")
total = sum(i * i for i in range(n))
return total
if __name__ == "__main__":
start = time.perf_counter()
# 方式 1:手动创建进程
p1 = multiprocessing.Process(target=cpu_bound, args=(10_000_000,))
p2 = multiprocessing.Process(target=cpu_bound, args=(10_000_000,))
p1.start(); p2.start()
p1.join(); p2.join()
print(f"双进程: {time.perf_counter() - start:.2f}s")
# 方式 2:进程池
with multiprocessing.Pool(processes=4) as pool:
results = pool.map(cpu_bound, [2_500_000] * 4)
print(f"总计: {sum(results)}")
# 方式 3:Pool 的其他方法
with multiprocessing.Pool(4) as pool:
# apply_async — 异步提交单个任务
future = pool.apply_async(cpu_bound, (10_000_000,))
result = future.get(timeout=30)
# imap — 惰性版 map
for result in pool.imap(cpu_bound, [1_000_000] * 10):
print(result)
19.3.2 进程间通信
import multiprocessing
# Queue — 进程安全队列
def producer(q):
for i in range(5):
q.put(f"数据-{i}")
q.put(None)
def consumer(q):
while True:
item = q.get()
if item is None:
break
print(f"收到: {item}")
if __name__ == "__main__":
q = multiprocessing.Queue()
p = multiprocessing.Process(target=producer, args=(q,))
c = multiprocessing.Process(target=consumer, args=(q,))
p.start(); c.start()
p.join(); c.join()
# Pipe — 双向管道(两个进程间通信)
def sender(conn):
conn.send("Hello from sender")
conn.close()
if __name__ == "__main__":
parent_conn, child_conn = multiprocessing.Pipe()
p = multiprocessing.Process(target=sender, args=(child_conn,))
p.start()
print(parent_conn.recv()) # Hello from sender
p.join()
# 共享内存
from multiprocessing import Value, Array
def increment(counter, lock):
for _ in range(100_000):
with lock:
counter.value += 1
if __name__ == "__main__":
counter = Value("i", 0) # 共享整数
lock = multiprocessing.Lock()
processes = [
multiprocessing.Process(target=increment, args=(counter, lock))
for _ in range(4)
]
for p in processes: p.start()
for p in processes: p.join()
print(f"结果: {counter.value}") # 400000
19.4 concurrent.futures 统一接口
concurrent.futures 为线程和进程提供统一的高层接口:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
def download(url):
time.sleep(1) # 模拟网络请求
return f"{url} 的内容"
def compute(n):
return sum(i * i for i in range(n))
# 线程池(I/O 密集型)
with ThreadPoolExecutor(max_workers=5) as executor:
urls = [f"https://example.com/{i}" for i in range(10)]
# map — 按顺序返回结果
results = executor.map(download, urls)
for result in results:
print(result)
# 进程池(CPU 密集型)
with ProcessPoolExecutor(max_workers=4) as executor:
# submit — 返回 Future 对象
futures = [executor.submit(compute, 1_000_000) for _ in range(8)]
# as_completed — 谁先完成谁先返回
from concurrent.futures import as_completed
for future in as_completed(futures):
print(f"结果: {future.result()}")
Future 对象
from concurrent.futures import ThreadPoolExecutor, Future
with ThreadPoolExecutor(3) as executor:
future: Future = executor.submit(download, "https://example.com")
print(future.done()) # False(可能还没完成)
print(future.running()) # True
result = future.result(timeout=5) # 阻塞等待结果
print(future.done()) # True
# 回调
def on_complete(f: Future):
print(f"完成: {f.result()}")
future2 = executor.submit(download, "https://example.com/2")
future2.add_done_callback(on_complete)
19.5 子解释器与 free-threaded Python(Python 3.13+)
子解释器(Subinterpreters)
每个子解释器有独立的 GIL,可以实现真正的线程并行:
# Python 3.12+ 的子解释器 API
import _interpreters as interpreters
interp_id = interpreters.create()
interpreters.run_string(interp_id, """
import math
print(f"子解释器中计算 pi = {math.pi}")
""")
interpreters.destroy(interp_id)
free-threaded Python(实验性,Python 3.13+)
Python 3.13 引入了实验性的无 GIL 构建(--disable-gil):
# 安装无 GIL 版本
# 需要从源码编译或使用特殊安装方式
python3.13t # t 后缀表示 free-threaded 版本
# 在 free-threaded Python 中,多线程 CPU 密集型任务可以真正并行
import threading
import sys
print(sys._is_gil_enabled()) # False(如果使用 free-threaded 构建)
# 现在多线程 CPU 密集型任务能获得真正的加速
注意:free-threaded Python 仍处于实验阶段,许多第三方 C 扩展尚不支持。
本章小结:Python 并发编程的选择取决于任务类型——I/O 密集用多线程或 asyncio,CPU 密集用多进程。
concurrent.futures提供了统一的高层接口,是大多数场景的首选。理解 GIL 是正确选择并发方案的关键。Python 3.13 的 free-threaded 模式预示着 Python 并发的未来。