目录
Python concurrent ——高效灵活并行计算的利剑
一、理解并发、并行与 concurrent 的角色
- 为什么需要并发?
现代计算机系统包含多个 CPU 核心,但许多程序是单线程执行的,导致 CPU 资源浪费。并发编程允许程序在等待 I/O 操作(如网络请求、文件读写)时,利用 CPU 执行其他任务,从而提高整体效率。 - 关键概念区分:
以下是关于并发、并行、同步、异步的概念总结表格:
| 概念 | 定义 | 特点/实现方式 | 适用场景 |
|---|---|---|---|
| 并发 (Concurrency) | 同时处理多个任务的能力 | 任务可交替执行(单CPU核心通过快速切换实现),给人“同时进行”的感觉 | I/O密集型任务 |
| 并行 (Parallelism) | 同时执行多个任务 | 需要多核CPU,任务真正在同一时刻运行 | CPU密集型任务 |
| 同步 (Synchronous) | 任务按顺序执行 | 一个任务完成后才开始下一个任务 | 任务间有强依赖关系时 |
| 异步 (Asynchronous) | 任务在等待时可让出控制权,执行其他任务 | 等待操作完成后再回来处理结果,concurrent.futures 提供基于线程/进程的同步接口管理异步操作 | 存在等待操作(如I/O)的场景 |
- I/O 密集型任务 🆚 CPU 密集型任务
- I/O 密集型任务: 网络请求(爬虫、API 调用)、文件读写、数据库查询。这些操作大部分时间花在等待网络传输或磁盘响应上,而不是 CPU 计算。
- CPU 密集型任务: 大量数学计算、图像处理、复杂算法。这些任务会持续占用 CPU。
目标: 提高程序的整体吞吐量和响应速度,更有效地利用系统资源。
concurrent模块是什么?concurrent.futures是 Python 标准库中提供的一个高级并发编程模块,旨在简化异步任务的执行。它为开发者提供了一个统一的接口来管理线程池和进程池,使并发编程更加简单、可维护。- 它的核心是
concurrent.futures子模块,提供了ThreadPoolExecutor和ProcessPoolExecutor两个主要的类。 - 它的核心价值是:
- 不用直接管理线程 (
threading.Thread) 或进程 (multiprocessing.Process) 的创建、启动、通信和清理。 - 提供了统一的接口 (
submit,map,as_completed) 来提交任务和获取结果。 - 内置了对任务结果 (
Future对象) 的管理机制。
- 不用直接管理线程 (
二、concurrent.futures 深度解析
concurrent.futures 是 concurrent 包中实际使用的部分。主要有学习两个执行器:
ThreadPoolExecutor: 线程池。ProcessPoolExecutor: 进程池。
它们都实现了 Executor 接口,因此 API 高度一致。
(一) Future 对象 - 并发的基石
1. Future定义
- 当你向执行器提交一个任务时,
submit()方法返回一个Future对象。这个对象代表了一个尚未完成的计算。
2. Future作用
| 具体说明 | |
|---|---|
| 占位符 | 作为未来某个时刻才会产生的结果的临时替代(占位) |
| 状态查询 | 可检查任务是否完成、是否被取消、是否抛出异常,掌握任务生命周期状态 |
| 结果获取 | 通过 result() 方法获取任务最终结果,或通过 exception() 方法获取异常 |
| 回调注册 | 任务完成后,自动调用预先指定的函数(回调函数),无需主动轮询结果 |
3. Future 的状态机
| 状态名称 | 描述说明 |
|---|---|
PENDING | 任务已提交,但尚未开始执行 |
RUNNING | 任务正在执行中 |
FINISHED | 任务已完成(包括成功完成、执行失败或被取消) |
4. Future 的关键方法
| 方法名称 | 返回值类型 | 描述说明 |
|---|---|---|
future.done() | bool | 检查任务是否完成(无论成功、失败或被取消) |
future.cancelled() | bool | 检查任务是否被成功取消 |
future.running() | bool | 检查任务是否正在运行 |
future.result(timeout=None) | Any | 获取任务结果。若任务未完成,会阻塞至任务完成或超时;如果任务抛出异常,result() 会重新抛出该异常。 |
future.exception(timeout=None) | Exception or None | 获取任务抛出的异常。若任务成功,返回None;若任务未完成,会阻塞至任务完成或超时 |
future.add_done_callback(fn) | 无 | 注册回调函数fn,当Future状态变为FINISHED时,自动调用fn(future)。注意:回调执行时机不确定(可能在主线程或工作线程/进程),应避免耗时操作或修改共享状态 |
5. Future基本用法
from concurrent.futures import Future
# 创建一个 Future 对象
f = Future()
# 模拟一个异步操作
def set_result():
f.set_result("Hello, Future!")
# 在另一个线程中设置结果
import threading
threading.Thread(target=set_result).start()
# 等待结果(阻塞)
print(f.result()) # 输出: Hello, Future!
(二)Executor 基类
1. Executor 定义
Executor 是一个抽象基类,定义了提交任务和获取结果的标准接口。ThreadPoolExecutor 和 ProcessPoolExecutor 都实现了这个接口。
2. Executor的关键方法
| 方法名称 | 核心功能 | 关键特性 | 适用场景 |
|---|---|---|---|
executor.submit(fn, /, *args, **kwargs) | 将可调用对象 fn 及其参数提交给执行器,调度任务执行 | 1. 立即返回 Future 对象,可后续查询状态/获取结果2. 支持单个任务的灵活提交 | 提交单个、参数不规则或独立的任务 |
executor.map(func, *iterables, timeout=None, chunksize=1) | 并发版 map,将 func 批量应用于 *iterables 的元素 | 1. 类似 zip(*iterables) 匹配元素,按输入顺序返回结果2. 支持超时设置,超时抛 TimeoutError3. chunksize 优化进程池 IPC 开销(对线程池无效)4. 遇第一个异常即停止 | 批量、同构(任务逻辑/参数结构一致)的任务 |
executor.shutdown(wait=True, *, cancel_futures=False) | 清理执行器资源(线程/进程) | 1. with 语句会自动调用,无需手动执行2. wait=True:阻塞至所有任务完成;wait=False:立即返回,后台续跑任务3. cancel_futures=True(Python 3.9+):取消所有待处理任务(不影响运行中任务) | 执行器不再使用时,释放资源 |
executor.submit(fn, /, *args, **kwargs) 示例代码:
import concurrent.futures
import time
def slow_task(n):
print(f"Task {n} starting...")
time.sleep(2)
print(f"Task {n} finished.")
return n * n
# 创建一个最多2个线程的线程池
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
# 提交任务,得到 Future 对象
future1 = executor.submit(slow_task, 1)
future2 = executor.submit(slow_task, 2)
future3 = executor.submit(slow_task, 3)
# 立刻打印 Future 对象,此时任务可能还没开始或还在运行
print(future1) # <Future at 0x... state=pending>
print(future2) # <Future at 0x... state=pending>
# 获取结果(会阻塞)
result1 = future1.result()
result2 = future2.result()
result3 = future3.result()
print(f"Results: {result1}, {result2}, {result3}")
executor.map(func, *iterables, timeout=None, chunksize=1) 示例代码:
import concurrent.futures
import requests
def fetch_url(url):
response = requests.get(url)
return len(response.content)
urls = [
'https://httpbin.org/delay/1',
'https://httpbin.org/delay/2',
'https://httpbin.org/delay/1'
]
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
# map 会并发执行 fetch_url,但结果按 urls 的顺序返回
results = executor.map(fetch_url, urls, timeout=10)
for url, length in zip(urls, results):
print(f"{url}: {length} bytes")
3. 两种主要执行器:线程池 🆚 进程池
ThreadPoolExecutor(线程池) | ProcessPoolExecutor(进程池) | |
|---|---|---|
| 核心原理 | 复用预先创建的线程执行任务,通过线程切换模拟“同时处理”,共享主线程内存空间。 | 复用预先创建的子进程执行任务,每个进程有独立解释器和内存,绕过 GIL 实现真正并行。 |
| 适用场景 | I/O 密集型任务(网络请求、文件操作、数据库访问等)—— I/O 等待时 GIL 释放,线程切换开销小。 | CPU 密集型任务(科学计算、图像处理、大数据分析等)—— 充分利用多核 CPU,无 GIL 限制。 |
| 关键构造参数 | - max_workers:默认 min(32, os.cpu_count() + 4)- thread_name_prefix:线程名称前缀(调试用)- initializer/initargs:线程启动时的初始化函数及参数 | - max_workers:默认 os.cpu_count()(通常设为 CPU 核心数)- mp_context:进程创建上下文(fork/spawn/forkserver)- initializer/initargs:进程启动时的初始化函数及参数 |
| 核心优点 | 1. 轻量级,线程创建/切换开销小 2. 线程间共享内存,数据交换方便 | 1. 绕过 GIL,支持真正并行计算 2. 进程内存隔离,无线程安全问题(如竞态条件) 3. 单个进程崩溃不影响主进程及其他进程 |
| 核心缺点 | 1. 受 GIL 限制,纯 CPU 密集型任务性能提升有限 2. 共享内存易引发线程安全问题,需额外同步(如 threading.Lock) | 1. 进程创建/切换开销大 2. 进程间通信(IPC)成本高(依赖 pickle 序列化) 3. 内存不共享,需用 multiprocessing 工具传递数据 |
ThreadPoolExecutor构造函数参数:
ThreadPoolExecutor(
max_workers=None,
thread_name_prefix='',
initializer=None,
initargs=()
)
max_workers: 线程池大小(默认 min(32, os.cpu_count() + 4))thread_name_prefix: 线程名称前缀initializer,initargs: 每个线程启动时调用的初始化函数
ThreadPoolExecutor示例:
import time
from concurrent.futures import ThreadPoolExecutor
def task(n):
print(f"Task {n} started")
time.sleep(n)
print(f"Task {n} completed")
return n * n
# 使用 with 语句自动管理资源
with ThreadPoolExecutor(max_workers=3) as executor:
# 提交任务,获取 Future 对象
future1 = executor.submit(task, 2)
future2 = executor.submit(task, 1)
future3 = executor.submit(task, 3)
# 获取结果(阻塞)
print(future1.result()) # 4
print(future2.result()) # 1
print(future3.result()) # 9
ProcessPoolExecutor构造函数参数
ProcessPoolExecutor(
max_workers=None,
mp_context=None,
initializer=None,
initargs=()
)
max_workers: 进程池大小(默认 os.cpu_count())mp_context: multiprocessing 上下文(‘fork’, ‘spawn’, ‘forkserver’)initializer,initargs: 每个进程启动时调用的初始化函数
ProcessPoolExecutor示例(CPU 密集型):
示例:进程池基础用法
import math
from concurrent.futures import ProcessPoolExecutor
def is_prime(n):
if n < 2:
return False
if n == 2:
return True
if n % 2 == 0:
return False
sqrt_n = int(math.isqrt(n))
for i in range(3, sqrt_n + 1, 2):
if n % i == 0:
return False
return True
if __name__ == '__main__': # Windows 需要这个
numbers = [112272535095293, 112582705942171, 112272535095293]
with ProcessPoolExecutor() as executor:
results = executor.map(is_prime, numbers)
for number, prime in zip(numbers, results):
print(f"{number} is prime: {prime}")
三、 高效与安全的并发编程
(一)concurrent.futures.as_completed(): 处理最先完成的任务
- 问题:
executor.map()按输入顺序返回结果。如果你希望一旦有任务完成就立即处理其结果,而不必等待前面慢的任务,该怎么办? - 解决方案:
as_completed(fs, timeout=None)- 接收一个
Future对象的可迭代集合fs。 - 返回一个迭代器,该迭代器按
Future完成的顺序(而不是提交的顺序)产生Future对象。 - 非常适合“谁先完成就处理谁”的场景。
import concurrent.futures import random import time def task_with_random_delay(name): delay = random.uniform(1, 5) time.sleep(delay) return f"Task {name} completed after {delay:.2f}s" with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # 提交多个任务 futures = [executor.submit(task_with_random_delay, f"T{i}") for i in range(5)] # 按完成顺序处理结果 for future in concurrent.futures.as_completed(futures): try: result = future.result() print(result) except Exception as exc: print(f"Task generated an exception: {exc}") - 接收一个
(二)concurrent.futures.wait(): 批量管理 Future 状态
- 功能: 同时等待一组
Future对象达到特定状态。 - 签名:
wait(fs, timeout=None, return_when=ALL_COMPLETED) - 参数:
fs:Future对象的集合。timeout: 超时时间(秒)。None表示无限等待。return_when: 决定何时返回。可选值:
return_when可选值 | 功能描述 |
|---|---|
FIRST_COMPLETED | 当任意一个 Future 完成(成功或失败)或被取消时,立即返回。 |
FIRST_EXCEPTION | 当任意一个 Future 抛出异常时,立即返回;若所有 Future 均无异常,则等待所有完成后返回(等价于 ALL_COMPLETED)。 |
ALL_COMPLETED | (默认值)当所有 Future 都完成(成功或失败)或被取消时,才返回。 |
- 返回值: 一个命名元组
(done, not_done),其中done是已完成的Future集合,not_done是尚未完成的Future集合。 - 应用场景:
- 实现超时控制。
- 等待至少一个任务成功完成。
- 批量检查状态。
import concurrent.futures import time def slow_task(name, success=True): time.sleep(2) if success: return f"Success: {name}" else: raise ValueError(f"Failed: {name}") with concurrent.futures.ThreadPoolExecutor() as executor: # 提交一个成功和一个失败的任务 future1 = executor.submit(slow_task, "FastSuccess", True) future2 = executor.submit(slow_task, "SlowFailure", False) # 等待,直到第一个异常出现 done, not_done = concurrent.futures.wait( [future1, future2], return_when=concurrent.futures.FIRST_EXCEPTION ) print("Done:", done) print("Not Done:", not_done) # 处理已完成的任务 for future in done: try: result = future.result() print("Result:", result) except Exception as exc: print("Exception:", exc)
(三)异常处理
- 关键原则: 任务中抛出的异常不会立即中断主线程。异常被封装在
Future对象中。 - 捕获异常:
- 调用
future.result()或future.exception()时,异常才会被重新抛出或获取。 - 必须在调用这些方法时使用
try-except块。
future = executor.submit(divide, 10, 0) try: result = future.result() except ZeroDivisionError as e: print(f"Caught division by zero: {e}") - 调用
map()的异常处理:executor.map()返回的迭代器在遍历时,如果某个任务抛出异常,该异常会在遍历到对应位置时由map迭代器抛出,并且迭代会停止。可以结合try-except在循环中处理。for arg, result in zip(args, executor.map(func, args)): try: print(f"{arg} -> {result}") except Exception as e: print(f"Error processing {arg}: {e}")
(四)资源管理与上下文管理器
- 强烈推荐使用
with语句创建执行器。它确保shutdown()方法一定会被调用,即使发生异常。with concurrent.futures.ThreadPoolExecutor() as executor: # ... 提交任务 ... pass # shutdown() 自动被调用 - 手动调用
shutdown():executor = concurrent.futures.ThreadPoolExecutor() try: # ... 提交任务 ... results = [future.result() for future in futures] finally: executor.shutdown(wait=True) # 确保等待完成
四、 实践、陷阱与替代方案
(一)实际应用模式
1. 批量下载/上传数据
import concurrent.futures
import requests
from pathlib import Path
def download_file(url, filename):
response = requests.get(url, stream=True)
response.raise_for_status()
with open(filename, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return f"Downloaded {filename}"
urls_and_files = [
('https://example.com/file1.zip', 'file1.zip'),
('https://example.com/file2.zip', 'file2.zip'),
# ...
]
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
# 提交所有下载任务
future_to_url = {
executor.submit(download_file, url, file): (url, file)
for url, file in urls_and_files
}
for future in concurrent.futures.as_completed(future_to_url):
url, file = future_to_url[future]
try:
result = future.result()
print(result)
except Exception as exc:
print(f"Downloading {url} to {file} generated an exception: {exc}")
2. 预取数据: 提交计算任务后,立即开始处理已经完成的任务,而不是等待所有任务。
with concurrent.futures.ProcessPoolExecutor() as executor:
futures = [executor.submit(cpu_intensive_func, data) for data in large_dataset]
for future in concurrent.futures.as_completed(futures):
try:
result = future.result()
process_result_immediately(result) # 一有结果就处理
except Exception as e:
log_error(e)
(二)常见陷阱与解决方案
1. 总结表格
| 陷阱名称 | 典型错误/场景 | 根本原因 | 解决方案 |
|---|---|---|---|
陷阱1:ProcessPoolExecutor中使用不可 pickle 的对象 | 抛出 AttributeError: Can't pickle local object '...' | 向进程池提交的任务中包含不可被 pickle 序列化的对象,如嵌套函数、lambda 表达式、带有不可序列化属性的类实例等(进程间通信依赖 pickle 传递数据)。 | 1. 将嵌套函数、lambda 移至模块顶层定义; 2. 使用可 pickle 的数据结构(如基础类型、标准库容器); 3. 重构代码,避免传递不可序列化对象。 |
| 陷阱2:忘记处理异常导致程序静默失败 | 程序无报错提示,但任务结果缺失、不完整或错误。 | 未通过 future.result() 或 future.exception() 获取任务状态,导致任务执行中抛出的异常被自动忽略,无法感知错误。 | 1. 调用 future.result() 时必须搭配 try-except 捕获异常;2. 使用 concurrent.futures.as_completed() 遍历 Future 对象,逐一处理结果与异常;3. 注册回调函数时,在回调内通过 future.exception() 检查异常。 |
陷阱3:在 initializer 中引发异常 | 线程池/进程池无法正常启动,或启动后任务执行异常。 | 执行器初始化时(initializer 函数)抛出未捕获的异常,导致工作线程/进程启动失败,进而影响整个执行器。 | 1. 确保 initializer 函数逻辑健壮,避免潜在异常;2. 在 initializer 内部添加 try-except 捕获异常并做好日志记录;3. 默认不使用 initializer 时,完全不存在这个陷阱。 |
| 陷阱4:死锁 | ThreadPoolExecutor 中,任务A等待任务B的结果,任务B因线程池无空闲线程无法启动,双方陷入无限等待。 | 池内任务通过 future.result() 阻塞等待同池内其他任务,且线程池 max_workers 不足,导致等待链闭环,无可用线程执行依赖任务。 | 1. 避免在池内任务中阻塞等待同池的其他 Future 结果;2. 若存在任务依赖,将被依赖任务移至主线程执行,或拆分任务逻辑减少耦合; 3. 适当调大 max_workers(需结合系统资源评估)。 |
陷阱5:Windows 上忘记 if __name__ == '__main__': | 抛出 RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase. | Windows 系统创建进程采用 spawn 方式,需重新导入主模块;若未用 if __name__ == '__main__': 包裹执行器代码,会导致模块重复执行,触发进程启动异常。 | 必须将 ProcessPoolExecutor 的创建、任务提交等逻辑,放入 if __name__ == '__main__': 代码块内。 |
2. 代码示例
以下是针对5个concurrent.futures陷阱的具体示例说明,每个陷阱都包含错误示例和正确示例:
陷阱1:在 ProcessPoolExecutor 中使用不可 pickle 的对象
错误示例(使用嵌套函数导致无法序列化):
import concurrent.futures
def main():
# 嵌套函数无法被pickle序列化
def process_data(data):
return data * 2
with concurrent.futures.ProcessPoolExecutor() as executor:
# 提交任务时会抛出PickleError
results = list(executor.map(process_data, [1, 2, 3]))
print(results)
if __name__ == "__main__":
main()
正确示例(将函数定义在模块顶层):
import concurrent.futures
# 定义在模块顶层的函数可被正常序列化
def process_data(data):
return data * 2
def main():
with concurrent.futures.ProcessPoolExecutor() as executor:
results = list(executor.map(process_data, [1, 2, 3]))
print(results) # 输出: [2, 4, 6]
if __name__ == "__main__":
main()
陷阱2:忘记处理异常导致程序静默失败
错误示例(未处理任务中抛出的异常):
import concurrent.futures
def risky_task(n):
if n == 2:
raise ValueError("数字不能为2") # 抛出异常
return n * 2
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = [executor.submit(risky_task, i) for i in [1, 2, 3]]
# 只获取了部分结果,忽略了异常
results = []
for future in futures:
# 未调用result()或exception(),异常被静默忽略
if future.done():
results.append(future.result())
print(results) # 可能输出不完整的结果,且无错误提示
正确示例(显式处理异常):
import concurrent.futures
def risky_task(n):
if n == 2:
raise ValueError("数字不能为2")
return n * 2
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = [executor.submit(risky_task, i) for i in [1, 2, 3]]
results = []
for future in concurrent.futures.as_completed(futures):
try:
result = future.result() # 触发异常
results.append(result)
except ValueError as e:
print(f"捕获异常: {e}") # 显式处理异常
print("成功结果:", results) # 输出: 成功结果: [2, 6]
陷阱3:在 initializer 中引发异常
错误示例(initializer函数抛出未处理异常):
import concurrent.futures
def bad_initializer():
# 初始化函数中抛出未处理异常
raise RuntimeError("初始化失败!")
# 执行器无法正常启动
with concurrent.futures.ThreadPoolExecutor(
initializer=bad_initializer
) as executor:
try:
# 提交任务会失败
future = executor.submit(lambda x: x*2, 10)
print(future.result())
except Exception as e:
print(f"执行器启动失败: {e}") # 输出初始化异常
正确示例(在initializer中处理异常):
import concurrent.futures
import logging
def safe_initializer():
try:
# 可能失败的初始化操作
logging.basicConfig(level=logging.INFO)
logging.info("初始化成功")
except Exception as e:
# 捕获并记录异常,避免执行器崩溃
logging.error(f"初始化警告: {e}")
with concurrent.futures.ThreadPoolExecutor(
initializer=safe_initializer
) as executor:
future = executor.submit(lambda x: x*2, 10)
print(future.result()) # 输出: 20 (即使初始化有警告也能正常执行)
默认不使用 initializer 时,完全不存在这个陷阱,因为没有初始化逻辑可引发异常。
陷阱4:死锁
错误示例(线程池内任务相互等待):
import concurrent.futures
def task1(executor):
# 任务1等待任务2的结果,但任务2需要线程池空闲才能执行
future = executor.submit(task2, 10)
return future.result() # 死锁点: 等待任务2完成,但无空闲线程执行任务2
def task2(n):
return n * 2
# 线程池大小为1,导致死锁
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
try:
# 提交任务1后,线程池已无空闲线程
result = executor.submit(task1, executor).result()
print(result)
except Exception as e:
print(f"发生死锁: {e}")
正确示例(避免池内任务相互依赖):
import concurrent.futures
def task1(value):
# 直接使用预计算的值,不依赖其他池内任务
return value * 2
def task2(n):
return n * 2
# 方案1: 增大线程池容量
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
# 任务1和任务2可并行执行
future2 = executor.submit(task2, 10)
future1 = executor.submit(task1, future2.result())
print(future1.result()) # 输出: 40
# 方案2: 拆分依赖关系,在主线程处理依赖
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
future2 = executor.submit(task2, 10)
value2 = future2.result() # 主线程等待任务2完成
future1 = executor.submit(task1, value2) # 再提交任务1
print(future1.result()) # 输出: 40
陷阱5:在 Windows 上忘记 if __name__ == '__main__':
错误示例(Windows环境下无主模块保护):
# 在Windows系统中运行会报错
import concurrent.futures
def process_data(n):
return n * 2
# 直接在全局作用域使用ProcessPoolExecutor
with concurrent.futures.ProcessPoolExecutor() as executor:
results = list(executor.map(process_data, [1, 2, 3]))
print(results)
# 错误: RuntimeError: An attempt has been made to start a new process...
正确示例(添加主模块保护):
# 在Windows系统中正常运行
import concurrent.futures
def process_data(n):
return n * 2
# 关键: 使用if __name__ == '__main__'包裹进程池代码
if __name__ == "__main__":
with concurrent.futures.ProcessPoolExecutor() as executor:
results = list(executor.map(process_data, [1, 2, 3]))
print(results) # 输出: [2, 4, 6]
这些示例清晰展示了每个陷阱的触发场景和对应的解决方案,实际开发中需根据具体场景选择合适的处理方式,尤其注意跨平台兼容性和异常处理。
(三)concurrent.futures vs asyncio
| 特性 | concurrent.futures (线程/进程) | asyncio |
|---|---|---|
| 模型 | 多线程/多进程 (抢占式) | 单线程事件循环 (协作式) |
| GIL | 线程受限制,进程不受限 | 不受限制 (I/O 等待时让出) |
| 适用 | I/O 密集 (线程), CPU 密集 (进程) | I/O 密集 |
| 复杂度 | 相对简单,同步风格 API | 较高,需理解 async/await |
| 阻塞 | result() 会阻塞线程 | await 挂起协程,不阻塞事件循环 |
| 数据共享 | 线程易,进程难 | 易 (单线程) |
| 库支持 | 广泛 | 需要 async 兼容库 (aiohttp, asyncpg) |
- 选择建议:
- 简单的 I/O 并发或 CPU 并行:
concurrent.futures。 - 高吞吐量的 I/O 服务(如 Web 服务器、大量并发连接):
asyncio。 - 混合场景:可以结合使用(例如,用
asyncio管理网络,用ProcessPoolExecutor处理 CPU 任务)。
- 简单的 I/O 并发或 CPU 并行:
(四)更高级的替代方案
multiprocessing.Pool:ProcessPoolExecutor的前身,功能类似,但 API 不同。concurrent.futures是更现代的推荐选择。joblib: 专为科学计算设计,对 numpy 数组优化好,语法更简洁(Parallel和delayed)。celery: 分布式任务队列,适用于跨机器、持久化、复杂工作流的场景。
总结
- 理解并发与并行的区别:
- 并发:同时处理多个任务(交替执行)
- 并行:同时执行多个任务(真正同时)
- 选择合适的执行器:
- I/O 密集型 →
ThreadPoolExecutor - CPU 密集型 →
ProcessPoolExecutor
- I/O 密集型 →
- 掌握核心 API:
submit():提交单个任务map():提交批量任务as_completed():按完成顺序处理结果wait():批量管理任务状态
- 正确处理异常:
- 在
result()或exception()调用时捕获异常 - 使用
as_completed时在循环中处理异常
- 在
- 注意平台差异:
- Windows 上使用
ProcessPoolExecutor时需在if __name__ == '__main__':中 - 确保传递的参数和返回值可序列化
- Windows 上使用
- 性能调优:
- 为 I/O 密集型任务设置较大的线程池
- 为 CPU 密集型任务设置与 CPU 核心数匹配的进程池
- 资源管理:
- 始终使用
with语句管理执行器
- 始终使用
1109

被折叠的 条评论
为什么被折叠?



