文章
ThreadPoolExecutor 知识点回顾
demo01
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
# 模拟一个I/O任务
def download_file(file_id):
time.sleep(1) # 假装在下载
return f"文件{file_id}下载完成"
# 创建线程池(最多3个线程)
# 等待所有任务完成再退出
with ThreadPoolExecutor(max_workers=3) as executor:
# 提交任务,立即返回Future对象
future1 = executor.submit(download_file, 1)
future2 = executor.submit(download_file, 2)
# 当需要结果时调用.result() 会阻塞直到任务完成
print(future1.result())
print(future2.result())
demo02
# 通常我们有一堆任务,谁先完成就先处理谁,不按提交顺序
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_url(url):
time.sleep({'a': 2, 'b': 1, 'c': 3}.get(url))
return f"{url} 完成"
urls = ['a', 'b', 'c']
with ThreadPoolExecutor(max_workers=3) as executor:
# 批量提交 用字典把 Future 和它的参数关联起来 以便知道结果来自哪个任务
futures = {executor.submit(fetch_url, url): url for url in urls}
# as_completed 迭代器:谁先完成就先yield谁
for future in as_completed(futures):
url = futures[future] # 同过先完成发future获取对应的url
try:
result = future.result()
print(f"[{url}] -> {result}")
except Exception as e:
print(f"[{url}] 出错:{e}")
demo03
import time
from concurrent.futures import ThreadPoolExecutor
def double(x):
time.sleep(0.5)
return x * 2
with ThreadPoolExecutor(max_workers=3) as executor:
# map 接收可迭代对象,返回结果的生成器(保持输入顺序)
results = executor.map(double, [1, 2, 3, 4, 5])
# 输出按 1 到 5 的顺序打印,但任务是并发执行的。
for num, res in zip([1, 2, 3, 4, 5], results):
print(f"double({num}) = {res}")
demo04
from concurrent.futures import ThreadPoolExecutor, as_completed
def risky_task(n):
if n == 2:
raise ValueError("n不能为2")
return n * 10
with ThreadPoolExecutor(max_workers=2) as executor:
futures = [executor.submit(risky_task, i) for i in range(1, 4)]
for future in as_completed(futures):
try:
print(future.result())
except Exception as e:
print(f"任务失败:{e}")
demo05
# 导入线程池执行器,用于方便地创建和管理线程
from concurrent.futures import ThreadPoolExecutor
# 导入互斥锁,用于保护共享资源,防止多个线程同时修改
from threading import Lock
# 全局计数器,所有线程都会修改它
counter = 0
# 创建一个互斥锁对象
lock = Lock()
def increment():
"""每个线程要执行的任务:将 counter 累加 10 万次"""
# 声明 counter 是全局变量,而不是局部变量
global counter
# 循环 100,000 次,每次对 counter 加 1
for _ in range(100000): # 10w
# 使用 with 语句获取锁,等效于 lock.acquire() 和 lock.release()
# 任何时刻只有一个线程能进入这个代码块,其他线程会在此等待
with lock: # 不加锁,最终结果会小于预期
counter += 1 # 临界区:该操作不是原子的,需要加锁保护
# 使用 with 创建线程池,max_workers=5 表示池中最多同时运行 5 个线程
with ThreadPoolExecutor(max_workers=5) as executor:
# 提交 10 个同样的任务到线程池
for _ in range(10): # 10 * 10w = 100w
# submit 方法将函数提交给线程池执行,立即返回 Future 对象(这里没有保存)
executor.submit(increment)
# 所有任务完成后(因为 with 会等待线程池关闭),打印最终计数
print(counter) # 输出:1000000demo06
import time
from concurrent.futures import ThreadPoolExecutor
def callback(future):
# 无论成功或失败这里都能拿到结果或异常
try:
result = future.result()
print(f"回调收到:{result}")
except Exception as e:
print(f"回调捕捉到异常:{e}")
with ThreadPoolExecutor(max_workers=2) as executor:
f = executor.submit(lambda : 1/0) # 会抛异常
f.add_done_callback(callback)
# 主线可以继续做其他实现
time.sleep(0.5)
demo07
import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_url(url):
try:
resp = requests.get(url, timeout=5)
return url, resp.status_code, len(resp.content)
except Exception as e:
return url, None, str(e)
urls = ["https://httpbin.org/delay/1"] * 5 # 5个慢请求
with ThreadPoolExecutor(max_workers=10) as executor:
future_to_url = {executor.submit(fetch_url, u): u for u in urls}
for future in as_completed(future_to_url):
url, status, length = future.result()
print(f"{url} -> 状态:{status},长度:{length}")
ThreadPoolExecutor vs ProcessPoolExecutor
| 特性 | ThreadPoolExecutor | ProcessPoolExecutor |
|---|---|---|
| 适用任务 | I/O 密集型 | CPU 密集型 |
| GIL 影响 | 受限于 GIL,CPU 并行不行 | 绕过 GIL,真正多核 |
| 内存/开销 | 轻量,共享内存 | 进程隔离,开销大 |
| 数据共享 | 需锁/队列 | 需 multiprocessing 机制 |
选择口诀:
“读写网络和磁盘,线程池里跑得欢;复杂计算要并行,进程池子才可行。”
常见坑与最佳实践
- max_workers 不是越大越好
- I/O 密集:可设 10~50,甚至更多,但受限于系统和目标服务器。
- 可以用
(cpu_count() * 5)作为初始参考。
- 不要在任务中修改共享状态不加锁
- 如全局列表、字典,多线程同时操作容易出 bug。
- Future.result() 阻塞主线程
- 需考虑是否需要先完成所有任务才能继续。需要实时响应就用
as_completed+ 回调。
- 需考虑是否需要先完成所有任务才能继续。需要实时响应就用
- 用
with语句保证资源释放- 即使发生异常,线程池也会等待任务完成并关闭。
- 任务内异常必须捕获
- 否则只能通过
future.result()才抛出,容易被忽略。
- 否则只能通过