进阶篇 · Advanced
lv.3 进阶
kp-030
线程与线程池
1. 一句话定义
threading 提供线程原语,concurrent.futures.ThreadPoolExecutor 是日常首选的线程池高层封装:提交任务、拿 Future、等结果——把 GIL 之下的 IO 并发写得像同步代码。
2. 为什么重要
混合 IO(下载、数据库、文件)的中等并发场景,线程池是性价比最高的方案:心智负担低于 asyncio、开销低于多进程。
3. 前置知识
kp-029(GIL 与选型)。
4. 核心概念
- Thread:
threading.Thread(target=fn, args=(...)),底层原语,手动管理。 - 线程池:
ThreadPoolExecutor(max_workers=8)+submit/map,返回 Future。 - 锁:
threading.Lock保护共享状态(GIL 不保证业务原子性)。 - 其他原语:
RLock(可重入)、Event(信号)、Semaphore(配额)、Queue(线程安全队列,首选的通信方式)。
5. 原理与机制
python
from concurrent.futures import ThreadPoolExecutor, as_completed
import urllib.request
urls = [f"https://example.com/{i}" for i in range(20)]
def fetch(u):
with urllib.request.urlopen(u, timeout=5) as r:
return u, r.status
with ThreadPoolExecutor(max_workers=8) as pool: # with 结束自动等待全部
futures = {pool.submit(fetch, u): u for u in urls}
for fut in as_completed(futures): # 谁先完成处理谁
u = futures[fut]
try:
print(u, fut.result()) # 取结果(可能重抛异常)
except Exception as e:
print(u, "失败:", e)pool.map(fetch, urls) 是保序版(类似 map,惰性交结果)。
共享状态加锁:
python
lock = threading.Lock()
count = 0
def bump():
global count
with lock: # with = acquire/finally release
count += 16. 关键事实(模型/图示)
text
submit(fn, *a) ──► Worker 线程执行 ──► Future 状态机
pending → running → done
result()/add_done_callback() 在完成侧收线
异常不丢:result() 处重新抛出
线程数经验:IO 密集 2-4×核数起步,网络爬取看目标限速7. 直观类比
线程池像叫号银行的 8 个柜员窗口:任务(客户)拿号排队(submit 返回 Future=叫号单),柜员先空先叫(as_completed),单子上有结果也有"办理失败"(异常重抛)。手写 Thread 是给每位客户单开一个柜台——20 个客户开 20 个柜台,成本爆炸。
8. 实例与案例
python
# 限制并发的批量下载 + 进度
def download_all(urls, workers=8):
results = []
with ThreadPoolExecutor(workers) as pool:
for i, res in enumerate(pool.map(fetch, urls), 1):
results.append(res)
print(f"{i}/{len(urls)}")
return results9. 常见误区
- 线程间直接改共享变量不加锁 —— check-then-act、
+=都可能丢更新;要么加锁,要么改用Queue传递。 - 忘记 with 导致进程不等线程退出 ——
ThreadPoolExecutor的上下文管理即"等所有任务完成"。 - 线程里抛异常以为没人知道 —— 异常存在 Future 里;不调
result()或检查exception()就静默丢失。 - 线程数越大越快 —— 超过阻塞比与目标承受度后只剩切换开销与被封风险;从 8 起步压测。
10. 自测题
- Future 是什么?
result()的两个副作用? pool.map与as_completed的差异?- 为什么共享计数器要用锁,GIL 不是保证安全吗?
参考答案
- 代表"尚未完成的结果"的句柄;副作用 = 阻塞等待完成、把工作线程的异常重新抛出。
- map 按提交顺序返回结果(慢任务阻塞后续交付);as_completed 按完成顺序交付(先到先得)。
- GIL 只保证单条字节码的原子性;
count += 1是多条字节码,中途切换即丢失更新。
11. 与其他知识点的关系
12. 延伸阅读
- concurrent.futures 文档(ThreadPoolExecutor/ProcessPoolExecutor 共用同一 API)