写Python的人多多少少都被"并发"这两个字绊过脚。线程也好、进程也罢,官方标准库里各种底层接口摆在那儿,threading、multiprocessing各管一摊,用起来总觉得隔着一层。concurrent.futures这个模块的出现,本质上就是给这堆底层机制套了一层统一的高级封装------不用再纠结线程池怎么创建、任务怎么排队、结果怎么取,一套接口打通线程和进程两条路。
下面就从概念到代码,把这个模块彻底捋一遍。
🧭 为什么需要concurrent.futures
Python有个众所周知的"性能瓶颈"叫GIL (全局解释器锁)。同一时刻,一个Python进程里只有一个线程能真正执行字节码,这意味着纯计算型任务用多线程基本白搭------CPU忙不过来的时候,线程们只是在互相让路,速度不会因为线程数增加而线性提升。
但如果任务大部分时间在等(等网络返回、等磁盘IO、等数据库响应),线程就派上用场了,因为等待期间GIL会被释放,别的线程能插空干活。
这就引出一个朴素但极其重要的判断标准
- IO密集型任务 → 用线程(
ThreadPoolExecutor) - CPU密集型任务 → 用进程(
ProcessPoolExecutor),绕开GIL的限制,让多个CPU核心真正同时算
如果用Amdahl定律的思路简单描述加速比的上限
S(n)=(1−p)+np1
其中p是可并行部分的比例,n是并行执行单元数。CPU密集任务如果能真正用多核并行(也就是p够大),进程池带来的收益会非常可观;而IO密集任务本质是"隐藏等待时间",收益逻辑完全不同。
concurrent.futures巧妙的地方在于,它把线程池和进程池抽象成同一套接口,你写业务代码时几乎不用关心底层用的是线程还是进程,换一行代码就能切换。
🔍 核心概念拆解
整个模块其实就靠几个角色撑起来,画张图看会更清楚

1. Executor------任务调度的总管
Executor是个抽象基类,不会直接被实例化,真正干活的是它的两个子类ThreadPoolExecutor和ProcessPoolExecutor。它俩接口完全一致,核心方法就三个
- submit(fn, args, kwargs) * 提交单个任务,立刻返回一个
Future对象,不阻塞 - *map(fn, iterables) 类似内置的
map(),但是并发执行,按*提交顺序*返回结果 - shutdown(wait=True) 关闭线程/进程池,释放资源
官方文档给的例子简洁到没什么好解释的
python
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(pow, 323, 1235)
print(future.result())
用with语句管理生命周期是标准做法,退出上下文时会自动调用shutdown(wait=True),等所有任务跑完再往下走。
2. Future------一张"未来会兑现的支票"
submit()返回的这个Future对象,可以理解成一张期票,代表某个还没完成、但将来一定会有结果的计算。它的常用方法
| 方法 | 作用 |
|---|---|
result(timeout=None) |
阻塞等待并拿到返回值,超时会抛TimeoutError |
done() |
查询任务是否已完成 |
cancel() |
尝试取消尚未开始执行的任务 |
exception() |
如果任务抛异常,取出这个异常对象 |
add_done_callback(fn) |
任务完成后自动触发回调函数 |
有意思的是,concurrent.futures.Future跟asyncio.Future长得很像但完全是两码事------前者用于线程/进程池的同步阻塞式获取,后者是为协程事件循环量身定制的,两者不能混用。
3. submit() 和 map() 到底差在哪
这是个特别容易踩坑的地方,Stack Overflow上有个高赞讨论专门讲这事。很多人写出下面这种代码,然后困惑为什么结果不对劲
ini
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(f, iterable))
# 直接拿到 f 的返回值列表
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(map(lambda x: executor.submit(f, x), iterable))
# 拿到的是一堆 Future 对象,还得再 .result() 一次
关键区别在这儿
- map() 返回结果是按提交顺序排列的,哪怕某个任务先完成,也得等前面的任务都出结果才会依次吐出来。适合"我知道顺序,只想要结果列表"的场景
- submit() 配合
as_completed()能做到谁先完成先处理谁,特别适合需要及时响应的场景,比如批量请求数据库、谁先返回就先处理谁
有个很直观的验证方法
python
import time
from concurrent.futures import ThreadPoolExecutor
e = ThreadPoolExecutor(4)
for i in e.map(time.sleep, range(10)):
print(i) # 顺序打印,但不会等所有任务全部结束才开始打印第一个
结果依然按顺序出来,但循环不会傻等所有任务跑完才继续,这说明map()底层其实也是并发调度的,只是对外表现出的顺序是"排队式"的。
4. as_completed() 与 wait()------两种收割结果的姿势
如果用submit()拿到一堆Future,想要"谁先完成就先处理谁",标准写法是
scss
from concurrent.futures import ThreadPoolExecutor, as_completed
with ThreadPoolExecutor(max_workers=4) as executor:
futures = [executor.submit(fetch_url, url) for url in url_list]
for future in as_completed(futures):
print(future.result())
as_completed()会持续产出已经完成 的Future,而不管它们原本的提交顺序,这正好弥补了map()死板排队的短板。
💡 一个完整的对比示例
假设有一批网络请求(IO密集)和一批质数判断计算(CPU密集),用两种池子分别处理,代码结构几乎一模一样,这就是concurrent.futures的魅力所在
python
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import requests
def fetch(url):
return requests.get(url).status_code
def is_prime(n):
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
urls = ["https://example.com"] * 10
numbers = [112272535095293, 112582705942171, 112272535095293]
# IO密集 → 线程池
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(fetch, urls))
# CPU密集 → 进程池
with ProcessPoolExecutor(max_workers=4) as executor:
results = list(executor.map(is_prime, numbers))
除了Thread换成Process,业务代码一行没变------这就是统一接口设计的价值。
⚠️ 一个容易踩的坑:死锁
官方文档特意警告过一种场景:如果某个任务在等待另一个还没提交完的任务 的结果,而线程池的max_workers不够用,就会死锁。
ini
executor = ThreadPoolExecutor(max_workers=2)
a = executor.submit(wait_on_b) # a 在等 b 的结果
b = executor.submit(wait_on_a) # b 在等 a 的结果
# 两者互相等待,谁都跑不完
这种"任务A等任务B、任务B又等任务A"的循环依赖,本质上和多线程加锁时的死锁是同一个道理,只是换了个马甲。实际写代码时,最好避免让提交到同一个池子里的任务互相依赖结果。
📊 选择建议一览
| 场景 | 推荐执行器 | 原因 |
|---|---|---|
| 网络请求、文件IO | ThreadPoolExecutor | 等待期间GIL释放,线程切换开销远小于进程 |
| 数值计算、图像处理 | ProcessPoolExecutor | 绕开GIL,真正利用多核 |
| 需要顺序结果 | executor.map() |
天然按提交顺序返回 |
| 需要谁先完成先处理谁 | executor.submit() + as_completed() |
灵活应对完成时间不确定的任务 |
🎯 写在最后
concurrent.futures的设计哲学其实特别朴素------别让开发者操心线程和进程的底层细节,先把"提交任务、拿到未来结果"这套流程标准化 。理解了Executor、Future、submit与map这几个核心概念,基本上就能应付日常开发里九成以上的并发需求。再往深了走,才会涉及asyncio的协程模型,那是另一套完全不同的并发哲学,值得单开一篇细聊。
参考资料
Python官方文档. concurrent.futures --- Launching parallel tasks . docs.python.org/3/library/c...
Stack Overflow. How does ThreadPoolExecutor().map differ from ThreadPoolExecutor().submit? stackoverflow.com/questions/2...
Katiyar, S. Introduction to concurrent.futures in Python . Medium. medium.com/@smrati.kat...
concurrent.futures --- Asynchronous computation (backport documentation). pythonhosted.org/futures/