程序变慢时,第一反应往往是"让它同时干更多活"。Python 提供了线程、进程、协程三套并发方案,选对方案比写出代码更重要。本文聚焦最常用的线程与协程,讲清原理、代码和适用场景。
1. 并发 vs 并行
并发是"交替执行",并行是"同时执行"。Python 的 GIL 让多线程无法真正并行跑 CPU 密集代码,但对网络、磁盘这类 IO 密集任务,线程依然能大幅提速:
import threading, time
def worker(name):
print(f"{name} 开始")
time.sleep(1)
print(f"{name} 结束")
t = threading.Thread(target=worker, args=("A",))
t.start()
t.join() # 等待线程结束
print("主线程继续")
2. 线程池:并发下载
手写线程管理容易出错,直接用 ThreadPoolExecutor 最省心:
from concurrent.futures import ThreadPoolExecutor
import time
def fetch(url):
time.sleep(0.5) # 模拟网络请求
return f"OK: {url}"
urls = [f"https://x.com/{i}" for i in range(8)]
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(fetch, urls))
print(results)
3. 线程安全与锁
多线程同时改同一个变量会互相覆盖,用 Lock 保护临界区:
import threading
counter = 0
lock = threading.Lock()
def add():
global counter
for _ in range(100_000):
with lock: # 同一时刻只有一个线程能进入
counter += 1
ts = [threading.Thread(target=add) for _ in range(4)]
[t.start() for t in ts]
[t.join() for t in ts]
print(counter) # 400000;不加锁结果可能不对
4. 协程:async/await
协程在等待 IO 时主动让出 CPU,单线程就能处理海量并发连接:
import asyncio
async def hello(name):
print(f"开始 {name}")
await asyncio.sleep(1) # 模拟 IO,让出控制权
print(f"完成 {name}")
async def main():
await asyncio.gather(
hello("A"), hello("B"), hello("C"),
)
asyncio.run(main()) # 三个任务总耗时约 1 秒
5. 实战:并发请求 API
用 aiohttp 写真正的异步 HTTP 客户端,10 个请求几乎同时发出:
import asyncio, aiohttp
async def fetch(session, url):
async with session.get(url) as resp:
return await resp.text()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, f"https://httpbin.org/get?i={i}")
for i in range(10)]
results = await asyncio.gather(*tasks)
print("完成", len(results), "个请求")
asyncio.run(main())
6. 怎么选
| 场景 | 推荐方案 |
|---|---|
| IO 密集(网络/文件),代码简单 | 线程 + ThreadPoolExecutor |
| IO 密集,连接数巨大 | 协程 asyncio |
| CPU 密集(计算/压缩) | 多进程 ProcessPoolExecutor |
| 混合任务 | 组合使用:进程内跑协程 |
💡 学习建议:先写一个"串行下载 10 个文件"的脚本,再分别用线程池和协程改造,对比总耗时。把 GIL、事件循环、await 这几个概念用实验钉死,并发就入门了。