>>>PyPathPython 学习站
首页›进阶篇›kp-030
进阶篇 · Advanced lv.3 进阶 kp-030

线程与线程池

前置知识:kp-029

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 += 1

6. 关键事实(模型/图示)

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 results

9. 常见误区

  1. 线程间直接改共享变量不加锁 —— check-then-act、+= 都可能丢更新;要么加锁,要么改用 Queue 传递。
  2. 忘记 with 导致进程不等线程退出 —— ThreadPoolExecutor 的上下文管理即"等所有任务完成"。
  3. 线程里抛异常以为没人知道 —— 异常存在 Future 里;不调 result() 或检查 exception() 就静默丢失。
  4. 线程数越大越快 —— 超过阻塞比与目标承受度后只剩切换开销与被封风险;从 8 起步压测。

10. 自测题

  1. Future 是什么?result() 的两个副作用?
  2. pool.map 与 as_completed 的差异?
  3. 为什么共享计数器要用锁,GIL 不是保证安全吗?
参考答案
  1. 代表"尚未完成的结果"的句柄;副作用 = 阻塞等待完成、把工作线程的异常重新抛出。
  2. map 按提交顺序返回结果(慢任务阻塞后续交付);as_completed 按完成顺序交付(先到先得)。
  3. GIL 只保证单条字节码的原子性;count += 1 是多条字节码,中途切换即丢失更新。

11. 与其他知识点的关系

  • kp-031 asyncio:更高并发的替代(万级连接)。
  • kp-032 多进程:CPU 密集的对应方案。
  • kp-025 上下文管理器:with 管理池生命周期的又一例。

12. 延伸阅读

  • concurrent.futures 文档(ThreadPoolExecutor/ProcessPoolExecutor 共用同一 API)