Python concurrent.futures 实战:用 ThreadPoolExecutor 并发处理 + as_completed 收结果

Python concurrent.futures 实战:用 ThreadPoolExecutor 并发处理 + as_completed 收结果

你手头有 200 个 URL 要抓,串行 requests.get 一个个跑,每个网络往返 300ms,光等待就是一分多钟,CPU 全程在打瞌睡。这类「一堆各自独立、主要在等 IO」的活儿,正是线程池的主场。但很多人第一次用 ThreadPoolExecutor 就踩坑:要么用 map 拿不到异常,要么用 submit 后不知道怎么优雅地边完成边收结果。这篇把这些坑一次讲清楚。

朴素写法:串行,慢到肉眼可见

先看最直白的版本,抓一批 URL 的状态码:

python 复制代码
import time
import requests

def fetch_status(url: str) -> int:
    r = requests.get(url, timeout=5)
    return r.status_code

urls = ["https://httpbin.org/delay/1"] * 10

start = time.perf_counter()
results = [fetch_status(u) for u in urls]  # 一个跑完才跑下一个
print(results, f"{time.perf_counter() - start:.1f}s")
# [200, 200, ...] 10.x s ------ 每个 delay 1 秒,10 个就是 10 秒

10 个请求跑了 10 秒。但这些请求彼此毫无依赖,完全可以同时发出去。

用 ThreadPoolExecutor.map 并发:能跑,但异常会「延迟爆炸」

第一个改进是把列表推导换成线程池的 map:

python 复制代码
from concurrent.futures import ThreadPoolExecutor

with ThreadPoolExecutor(max_workers=10) as pool:
    results = list(pool.map(fetch_status, urls))
# 10 个请求几乎同时发出,总耗时约 1 秒

max_workers=10 表示最多 10 个线程同时干活,with 退出时会自动等所有任务结束再关池。速度从 10 秒降到 1 秒。

map 有个隐蔽问题:它保持输入顺序,而且异常会在你迭代到那一项时才抛出 。如果第 3 个 URL 挂了,前两个结果你都拿到了,迭代到第 3 个时才 raise,这时候想知道「到底哪些成功了」就很别扭。而且 map 一旦某项抛异常,后面的结果你也拿不到了。

正解:submit + as_completed,谁先好谁先拿

生产里更常用的是 submit 提交任务拿到 Future,再用 as_completed 按「完成先后」而非「提交顺序」收结果。这样先跑完的先处理,慢的不拖累快的:

python 复制代码
from concurrent.futures import ThreadPoolExecutor, as_completed

def fetch_status(url: str) -> int:
    r = requests.get(url, timeout=5)
    return r.status_code

urls = [
    "https://httpbin.org/delay/1",
    "https://httpbin.org/status/404",
    "https://httpbin.org/delay/2",
    "https://not-a-real-host.invalid",  # 故意让它失败
]

results, errors = {}, {}
with ThreadPoolExecutor(max_workers=8) as pool:
    # future 反查 url,方便出错时知道是谁挂了
    future_to_url = {pool.submit(fetch_status, u): u for u in urls}
    for future in as_completed(future_to_url):
        url = future_to_url[future]
        try:
            results[url] = future.result()  # 任务里抛的异常在这里重新抛出
        except Exception as e:
            errors[url] = repr(e)

print("成功:", results)
print("失败:", errors)

关键点在于 future.result():如果任务函数内部抛了异常,异常被 Future 兜住,直到你调 result() 才重新抛出。所以用 try/except 包住它,就能把「成功的」和「失败的」分开收集,一个失败不影响其余任务。as_completed 则保证谁先跑完谁先进循环,长尾任务不会阻塞短任务的处理。

给任务加超时,别让一个卡死拖垮整批

result(timeout=...) 可以给单个任务设等待上限。注意超时不会真的杀死线程(Python 线程无法强制中断),它只是让你别再干等:

python 复制代码
from concurrent.futures import TimeoutError as FutureTimeout

with ThreadPoolExecutor(max_workers=8) as pool:
    futures = {pool.submit(fetch_status, u): u for u in urls}
    for future in as_completed(futures, timeout=10):  # 整批 10 秒内必须收完
        url = futures[future]
        try:
            print(url, future.result(timeout=3))  # 单个任务最多等 3 秒
        except FutureTimeout:
            print(url, "该任务超时,先跳过")
        except Exception as e:
            print(url, "失败:", e)

真正的超时控制还得靠底层库自己支持,比如 requests.get(url, timeout=5)------线程池的 timeout 只是让主线程别一直傻等。

线程池 vs 进程池:选错等于白干

concurrent.futures 还提供 ProcessPoolExecutor,接口几乎一样,但适用场景相反:

python 复制代码
from concurrent.futures import ProcessPoolExecutor

def cpu_heavy(n: int) -> int:
    return sum(i * i for i in range(n))  # 纯计算,吃 CPU

# CPU 密集用进程池,绕开 GIL,真正并行
with ProcessPoolExecutor(max_workers=4) as pool:
    print(list(pool.map(cpu_heavy, [10**6] * 4)))

判断标准很简单:

  • IO 密集 (网络请求、读写文件、查数据库):线程主要在「等」,GIL 在等待时会释放,用 ThreadPoolExecutor,max_workers 可以开得比核心数大很多(几十到上百)。
  • CPU 密集 (加密、图像处理、大量数学计算):线程被 GIL 卡住无法并行,必须用 ProcessPoolExecutor,max_workers 一般设成 CPU 核心数。

用线程池跑 CPU 密集任务,你会发现开多少线程都不快------因为 GIL 让它们始终只有一个在真正执行。

小结

  • 独立的 IO 任务用 ThreadPoolExecutor,一行 with 自动管理线程生命周期。
  • 优先 submit + as_completed,谁先完成谁先处理;用 {future: 上下文} 字典反查是哪个任务出的错。
  • future.result() 才是异常真正抛出的地方,用 try/except 包住它就能把成功与失败分流,单个失败不拖垮整批。
  • result(timeout=) 只是让主线程别干等,真超时靠底层库(如 requeststimeout)。
  • 一句话记忆:等 IO 用线程池,算 CPU 用进程池,收结果认准 as_completed
相关推荐
爱码小白1 小时前
importlib模块
开发语言·前端·python
空杆推不起1 小时前
穿透数据库 JOIN 核心:语法分类、底层算法、生产优化与高频坑点全解析
数据库·oracle
axinawang2 小时前
第25课 for循环的应用
python
逻极2 小时前
FastAPI 实战:从入门到自动化文档,如何把API开发效率提升200%
python·api·fastapi·swagger·异步
xqqxqxxq2 小时前
MySQL 数据类型笔记
数据库·笔记·mysql
风样滴男人哟2 小时前
PHP特性之反射类ReflectionClass机制
android·开发语言·php
半兽先生2 小时前
大模型技术开发与应用——5.大模型Agent开发(CrewAI)
大数据·人工智能·python·机器学习·ai
眼泪划过的星空2 小时前
快速了解LangGraph:构建智能Agent工作流的核心框架
人工智能·python·langchain
Logintern092 小时前
理解MySQL数据库的“事务与锁”
数据库·mysql