9 minutes
并发编程:多线程与多进程
并发编程概述
并发编程是提升程序性能的关键手段。Python 提供了三种主要的并发模型:
- 多线程(threading):适用于 I/O 密集型任务
- 多进程(multiprocessing):适用于 CPU 密集型任务
- 异步(asyncio):适用于高并发 I/O 任务(下一章详解)
选择合适的并发模型至关重要——选错了不仅不会提升性能,反而可能让程序更慢。
理解并发与并行
# 并发:多个任务交替执行(看起来像同时)
# 并行:多个任务真正同时执行(多核 CPU)
# 类比:
# - 并发 = 一个人交替做三件事
# - 并行 = 三个人各做一件事
线程 vs 进程
| 特性 | 线程 (Thread) | 进程 (Process) |
|---|---|---|
| 内存共享 | 共享同一进程内存 | 独立内存空间 |
| 创建开销 | 较低 | 较高 |
| 通信方式 | 直接访问共享变量 | Queue/Pipe/共享内存 |
| 适合场景 | I/O 密集型 | CPU 密集型 |
| GIL 影响 | 受 GIL 限制 | 不受 GIL 限制 |
| 安全性 | 需要锁来保护共享数据 | 天然隔离 |
GIL 详解
GIL(Global Interpreter Lock,全局解释器锁)是 Python 中最重要的并发概念:
# GIL 确保同一时刻只有一个线程在执行 Python 字节码
# 这意味着:多线程无法利用多核 CPU 加速 CPU 密集型任务
# I/O 密集型任务:线程遇到 I/O 会释放 GIL,因此多线程有效
# CPU 密集型任务:线程一直占用 CPU,无法释放 GIL,因此多线程无效
# 解决方案:
# - I/O 密集型 → 多线程 / asyncio
# - CPU 密集型 → 多进程
threading 模块
创建线程
import threading
import time
# 方式1:函数方式
def worker(name, delay):
print(f"线程 {name} 启动")
time.sleep(delay)
print(f"线程 {name} 结束")
threads = []
for i in range(3):
t = threading.Thread(target=worker, args=(f"T{i}", i))
threads.append(t)
t.start()
# 等待所有线程结束
for t in threads:
t.join()
print("所有线程完成")
# 方式2:继承 Thread 类
class WorkerThread(threading.Thread):
def __init__(self, name, delay):
super().__init__()
self.name = name
self.delay = delay
def run(self):
print(f"线程 {self.name} 启动")
time.sleep(self.delay)
print(f"线程 {self.name} 结束")
threads = [WorkerThread(f"W{i}", i) for i in range(3)]
for t in threads:
t.start()
for t in threads:
t.join()
守护线程(Daemon Thread)
import threading
import time
def background_task():
while True:
print("后台任务运行中...")
time.sleep(1)
# 守护线程:主线程结束时自动退出
daemon = threading.Thread(target=background_task, daemon=True)
daemon.start()
time.sleep(3)
print("主线程结束")
# 守护线程会自动退出
线程状态
import threading
import time
def worker():
time.sleep(1)
t = threading.Thread(target=worker)
print(f"启动前: {t.is_alive()}") # False
t.start()
print(f"运行中: {t.is_alive()}") # True
t.join()
print(f"结束后: {t.is_alive()}") # False
# 当前活跃线程数
print(f"活跃线程: {threading.active_count()}")
# 当前线程
print(f"当前线程: {threading.current_thread().name}")
线程同步
Lock(互斥锁)
import threading
import time
# 共享资源
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100000):
# with 语句自动获取和释放锁
with lock:
counter += 1
# 如果没有锁,两个线程同时修改会导致 race condition
threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"最终计数: {counter}") # 1000000
RLock(可重入锁)
import threading
# Lock:同一个线程重复 acquire 会死锁
# RLock:同一个线程可以多次 acquire
lock = threading.Lock()
rlock = threading.RLock()
def recursive_with_lock(n):
"""使用 Lock 会导致死锁"""
with lock:
if n > 0:
recursive_with_lock(n - 1) # 第二次 acquire → 死锁!
def recursive_with_rlock(n):
"""使用 RLock 可以重入"""
with rlock:
print(f"深度: {n}")
if n > 0:
recursive_with_rlock(n - 1)
recursive_with_rlock(3) # 正常运行
条件变量(Condition)
import threading
import time
# 生产者-消费者模式
buffer = []
MAX_SIZE = 5
condition = threading.Condition()
def producer():
for i in range(10):
with condition:
while len(buffer) >= MAX_SIZE:
print("缓冲区已满,生产者等待...")
condition.wait()
buffer.append(f"物品-{i}")
print(f"生产: 物品-{i}, 缓冲区大小: {len(buffer)}")
condition.notify() # 通知消费者
time.sleep(0.1)
def consumer():
for _ in range(10):
with condition:
while len(buffer) == 0:
print("缓冲区为空,消费者等待...")
condition.wait()
item = buffer.pop(0)
print(f"消费: {item}, 缓冲区大小: {len(buffer)}")
condition.notify() # 通知生产者
time.sleep(0.2)
prod = threading.Thread(target=producer)
cons = threading.Thread(target=consumer)
prod.start()
cons.start()
prod.join()
cons.join()
信号量(Semaphore)
import threading
import time
# 控制同时访问资源的线程数量
semaphore = threading.Semaphore(3) # 最多3个线程同时访问
def limited_access(name):
with semaphore:
print(f"{name} 获得访问权限")
time.sleep(1)
print(f"{name} 释放访问权限")
threads = [threading.Thread(target=limited_access, args=(f"T{i}",))
for i in range(6)]
for t in threads:
t.start()
for t in threads:
t.join()
Event(事件)
import threading
import time
# 用于线程间的一个简单信号
event = threading.Event()
def waiter():
print("等待事件...")
event.wait() # 阻塞直到事件被设置
print("事件已触发!")
def setter():
time.sleep(2)
print("触发事件")
event.set()
t1 = threading.Thread(target=waiter)
t2 = threading.Thread(target=setter)
t1.start()
t2.start()
t1.join()
t2.join()
线程安全队列
queue.Queue 是线程安全的生产者-消费者队列:
import threading
import time
from queue import Queue
def producer(q: Queue, num_items: int):
for i in range(num_items):
item = f"数据-{i}"
q.put(item)
print(f"生产: {item}")
time.sleep(0.1)
q.put(None) # 发送结束信号
def consumer(q: Queue, name: str):
while True:
item = q.get()
if item is None: # 收到结束信号
q.put(None) # 传递给下一个消费者
break
print(f"{name} 消费: {item}")
time.sleep(0.2)
q = Queue(maxsize=5)
prod = threading.Thread(target=producer, args=(q, 10))
cons1 = threading.Thread(target=consumer, args=(q, "消费者1"))
cons2 = threading.Thread(target=consumer, args=(q, "消费者2"))
prod.start()
cons1.start()
cons2.start()
prod.join()
cons1.join()
cons2.join()
ThreadPoolExecutor
推荐使用 concurrent.futures.ThreadPoolExecutor 管理线程池:
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def fetch_data(url: str) -> str:
"""模拟网络请求"""
time.sleep(1)
return f"数据来自 {url}"
# 创建线程池
with ThreadPoolExecutor(max_workers=5) as executor:
urls = [f"http://api.example.com/data/{i}" for i in range(10)]
# 方式1:submit 逐个提交
futures = [executor.submit(fetch_data, url) for url in urls]
for future in as_completed(futures):
result = future.result()
print(result)
# 方式2:map 批量提交
# results = executor.map(fetch_data, urls)
# for result in results:
# print(result)
# 线程池自动关闭
print("所有请求完成")
实战:并行下载
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
import urllib.request
def download_url(url: str) -> tuple[str, int]:
"""下载 URL 并返回 (url, 字节数)"""
with urllib.request.urlopen(url, timeout=10) as response:
data = response.read()
return url, len(data)
urls = [
"https://www.python.org",
"https://www.github.com",
"https://www.stackoverflow.com",
"https://pypi.org",
]
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(download_url, url): url for url in urls}
for future in as_completed(futures):
url, size = future.result()
print(f"下载完成: {url} ({size} 字节)")
elapsed = time.perf_counter() - start
print(f"总耗时: {elapsed:.2f}秒")
multiprocessing 模块
创建进程
import multiprocessing
import time
def cpu_intensive(n: int) -> int:
"""CPU 密集型计算"""
total = 0
for i in range(n):
total += i ** 2
return total
# 方式1:函数方式
if __name__ == "__main__":
processes = []
for i in range(4):
p = multiprocessing.Process(target=cpu_intensive, args=(10_000_000,))
processes.append(p)
p.start()
for p in processes:
p.join()
print("所有进程完成")
重要提示: 在 Windows 上,多进程代码必须放在 if __name__ == "__main__": 块中,防止子进程重新执行模块代码。
ProcessPoolExecutor
from concurrent.futures import ProcessPoolExecutor
import time
def compute_square(n: int) -> int:
"""计算平方和"""
return sum(i ** 2 for i in range(n))
if __name__ == "__main__":
numbers = [5_000_000, 10_000_000, 15_000_000, 20_000_000]
start = time.perf_counter()
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(compute_square, numbers))
elapsed = time.perf_counter() - start
for n, result in zip(numbers, results):
print(f"sum(i^2 for i in range({n})) = {result}")
print(f"多进程耗时: {elapsed:.2f}秒")
multiprocessing.Pool
import multiprocessing
import time
def worker(n: int) -> int:
return n * n
if __name__ == "__main__":
with multiprocessing.Pool(processes=4) as pool:
# map:批量执行
results = pool.map(worker, range(10))
print(f"map 结果: {results}")
# apply:单个执行
result = pool.apply(worker, args=(42,))
print(f"apply 结果: {result}")
# starmap:多个参数
results = pool.starmap(pow, [(2, 10), (3, 5), (4, 3)])
print(f"starmap 结果: {results}")
# imap:惰性求值
for result in pool.imap(worker, range(5)):
print(f"imap: {result}")
进程间通信
Queue(队列)
import multiprocessing
import time
def producer(q: multiprocessing.Queue):
for i in range(5):
q.put(f"消息-{i}")
time.sleep(0.1)
q.put(None) # 结束信号
def consumer(q: multiprocessing.Queue, name: str):
while True:
item = q.get()
if item is None:
q.put(None) # 传递结束信号
break
print(f"{name} 收到: {item}")
if __name__ == "__main__":
q = multiprocessing.Queue()
p1 = multiprocessing.Process(target=producer, args=(q,))
p2 = multiprocessing.Process(target=consumer, args=(q, "消费者A"))
p1.start()
p2.start()
p1.join()
p2.join()
Pipe(管道)
import multiprocessing
def sender(conn: multiprocessing.Connection):
conn.send("你好")
conn.send([1, 2, 3])
conn.close()
def receiver(conn: multiprocessing.Connection):
while True:
try:
msg = conn.recv()
print(f"收到: {msg}")
except EOFError:
break
if __name__ == "__main__":
parent_conn, child_conn = multiprocessing.Pipe()
p1 = multiprocessing.Process(target=sender, args=(parent_conn,))
p2 = multiprocessing.Process(target=receiver, args=(child_conn,))
p1.start()
p2.start()
p1.join()
p2.join()
共享状态
import multiprocessing
def worker_counter(counter: multiprocessing.Value, lock: multiprocessing.Lock):
for _ in range(10000):
with lock:
counter.value += 1
def worker_dict(shared_dict: multiprocessing.Manager().dict, key: str):
shared_dict[key] = key * 10
if __name__ == "__main__":
# Value:共享数值
counter = multiprocessing.Value("i", 0) # 'i' = 有符号整数
lock = multiprocessing.Lock()
processes = [
multiprocessing.Process(target=worker_counter, args=(counter, lock))
for _ in range(4)
]
for p in processes:
p.start()
for p in processes:
p.join()
print(f"计数结果: {counter.value}") # 40000
# Manager:共享复杂数据结构
with multiprocessing.Manager() as manager:
shared_dict = manager.dict()
processes = [
multiprocessing.Process(target=worker_dict, args=(shared_dict, f"key-{i}"))
for i in range(5)
]
for p in processes:
p.start()
for p in processes:
p.join()
print(f"共享字典: {dict(shared_dict)}")
实战案例
1. 并行下载(多线程)
from concurrent.futures import ThreadPoolExecutor, as_completed
import time
def download_file(url: str) -> tuple[str, str]:
"""模拟文件下载"""
time.sleep(0.5) # 模拟网络延迟
content = f"这是 {url} 的内容"
return url, content
def parallel_download(urls: list[str], max_workers: int = 5) -> dict[str, str]:
"""并行下载多个文件"""
results = {}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = {executor.submit(download_file, url): url for url in urls}
for future in as_completed(futures):
url = futures[future]
try:
_, content = future.result()
results[url] = content
print(f"✓ 完成: {url}")
except Exception as e:
print(f"✗ 失败: {url} - {e}")
return results
urls = [f"https://example.com/file-{i}.txt" for i in range(10)]
start = time.perf_counter()
results = parallel_download(urls)
print(f"耗时: {time.perf_counter() - start:.2f}秒")
2. 并行计算(多进程)
from concurrent.futures import ProcessPoolExecutor
import time
import math
def is_prime(n: int) -> bool:
"""判断素数"""
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
for i in range(3, int(math.sqrt(n)) + 1, 2):
if n % i == 0:
return False
return True
def find_primes_in_range(start: int, end: int) -> list[int]:
"""在指定范围内查找素数"""
return [n for n in range(start, end) if is_prime(n)]
if __name__ == "__main__":
# 单进程 vs 多进程
total_range = 100_000
num_workers = 4
chunk_size = total_range // num_workers
ranges = [(i * chunk_size, (i + 1) * chunk_size) for i in range(num_workers)]
# 多进程
start = time.perf_counter()
with ProcessPoolExecutor(max_workers=num_workers) as executor:
futures = [executor.submit(find_primes_in_range, s, e) for s, e in ranges]
results = []
for future in futures:
results.extend(future.result())
elapsed = time.perf_counter() - start
print(f"多进程: 找到 {len(results)} 个素数, 耗时 {elapsed:.2f}秒")
3. 竞态条件演示
import threading
import time
# 没有锁的情况(竞态条件)
counter_without_lock = 0
def race_worker():
global counter_without_lock
for _ in range(100000):
# 读取 → 修改 → 写入(非原子操作)
temp = counter_without_lock
counter_without_lock = temp + 1
threads = [threading.Thread(target=race_worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"无锁: 期望 500000, 实际 {counter_without_lock}")
# 输出可能是: 无锁: 期望 500000, 实际 382941(每次不同)
# 有锁的情况
counter_with_lock = 0
lock = threading.Lock()
def safe_worker():
global counter_with_lock
for _ in range(100000):
with lock:
counter_with_lock += 1
threads = [threading.Thread(target=safe_worker) for _ in range(5)]
for t in threads:
t.start()
for t in threads:
t.join()
print(f"有锁: 期望 500000, 实际 {counter_with_lock}")
# 输出: 有锁: 期望 500000, 实际 500000
4. 线程 vs 进程性能对比
import time
import threading
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
def io_task():
"""模拟 I/O 密集型任务"""
time.sleep(1) # 模拟 I/O 等待
return "完成"
def cpu_task(n: int) -> int:
"""模拟 CPU 密集型任务"""
result = 0
for i in range(n):
result += i ** 2
return result
def compare_performance():
num_tasks = 8
# I/O 密集型
print("=== I/O 密集型任务 ===")
# 串行
start = time.perf_counter()
for _ in range(num_tasks):
io_task()
serial_io = time.perf_counter() - start
# 多线程
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=num_tasks) as ex:
list(ex.map(lambda _: io_task(), range(num_tasks)))
thread_io = time.perf_counter() - start
print(f"串行: {serial_io:.2f}s")
print(f"多线程: {thread_io:.2f}s")
print(f"加速比: {serial_io / thread_io:.1f}x")
# CPU 密集型
print("\n=== CPU 密集型任务 ===")
n = 5_000_000
# 串行
start = time.perf_counter()
for _ in range(num_tasks):
cpu_task(n)
serial_cpu = time.perf_counter() - start
# 多线程(受 GIL 限制)
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=num_tasks) as ex:
list(ex.map(cpu_task, [n] * num_tasks))
thread_cpu = time.perf_counter() - start
# 多进程
start = time.perf_counter()
with ProcessPoolExecutor(max_workers=num_tasks) as ex:
list(ex.map(cpu_task, [n] * num_tasks))
process_cpu = time.perf_counter() - start
print(f"串行: {serial_cpu:.2f}s")
print(f"多线程: {thread_cpu:.2f}s")
print(f"多进程: {process_cpu:.2f}s")
print(f"多进程加速比: {serial_cpu / process_cpu:.1f}x")
if __name__ == "__main__":
compare_performance()
选择指南
如何选择并发模型
# 决策树(伪代码)
def choose_concurrency_model(task_type: str):
if task_type == "I/O_bound":
if need_true_parallelism:
return "asyncio" # 单线程事件循环
else:
return "threading" # 多线程
elif task_type == "CPU_bound":
return "multiprocessing" # 多进程
elif task_type == "mixed":
return "组合使用"
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| Web API 请求 | asyncio / threading | I/O 密集型,高并发 |
| 文件读写 | threading / asyncio | I/O 密集型 |
| 数值计算 | multiprocessing | CPU 密集型,需多核 |
| 图像处理 | multiprocessing | CPU 密集型 |
| Web 服务器 | asyncio | 高并发 I/O |
| 爬虫 | asyncio / threading | I/O 密集型 |
| 数据处理流水线 | multiprocessing | CPU 密集型 |
常见陷阱
陷阱1:线程中的异常
import threading
def bad_worker():
raise ValueError("线程中出错")
t = threading.Thread(target=bad_worker)
t.start()
t.join()
# 异常不会传播到主线程!线程会静默退出。
# 解决方案:显式捕获并处理
def safe_worker():
try:
# 业务逻辑
raise ValueError("线程中出错")
except Exception as e:
print(f"线程出错: {e}")
陷阱2:死锁
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def worker_1():
with lock_a:
print("Worker 1 持有 lock_a")
with lock_b:
print("Worker 1 持有 lock_b")
def worker_2():
with lock_b:
print("Worker 2 持有 lock_b")
with lock_a: # 可能死锁!
print("Worker 2 持有 lock_a")
# 解决方案:始终以相同顺序获取锁
def worker_2_fixed():
with lock_a: # 与 worker_1 顺序一致
print("Worker 2 持有 lock_a")
with lock_b:
print("Worker 2 持有 lock_b")
陷阱3:进程间不能共享普通变量
import multiprocessing
# BAD: 普通变量不共享
counter = 0
def increment():
global counter
counter += 1
if __name__ == "__main__":
processes = [multiprocessing.Process(target=increment) for _ in range(5)]
for p in processes:
p.start()
for p in processes:
p.join()
print(counter) # 输出 0!子进程有自己的 counter
# GOOD: 使用 Value 或 Manager
def increment_safe(counter):
counter.value += 1
小结
并发编程是提升程序性能的重要武器,但需要正确选择工具。本章我们深入学习了 Python 的三种并发模型:多线程(threading)适用于 I/O 密集型任务,多进程(multiprocessing)适用于 CPU 密集型任务。我们掌握了线程创建与管理、锁机制(Lock/RLock)、线程间通信(Condition/Queue/Event)、线程池(ThreadPoolExecutor)、进程池(ProcessPoolExecutor/Pool)、进程间通信(Queue/Pipe)以及共享状态的方法。通过竞态条件演示、并行下载、并行计算和性能对比等实战案例,你应该清楚地理解了何时使用多线程、何时使用多进程。最后,GIL 不是魔鬼——理解它,你就能做出正确的并发技术选择。
Summary: 多线程、多进程、GIL、锁同步、进程间通信与实战对比。