第一章:并发编程的基石与 concurrent.futures
的诞生背景
欢迎来到Python并发编程的深邃世界。在本章中,我们将首先建立对并发编程的根本理解,剖析其核心概念,并在此背景下,探究concurrent.futures
库为何应运而生,以及它在Python并发生态系统中的独特地位。我们不只是学习API,而是要从最底层开始,理解其背后哲学和设计初衷。
1.1 并发、并行、异步:概念的精确定位
在深入concurrent.futures
之前,我们必须厘清几个经常被混淆但意义截然不同的概念:并发(Concurrency)、并行(Parallelism)和异步(Asynchrony)。它们是构建高性能、高响应度应用程序的基石。
1.1.1 并发 (Concurrency)
并发是指在同一时间段内处理多个任务的能力。它不意味着这些任务在物理上同时执行,而是指系统能够交错地执行这些任务,给人一种它们同时进行的错觉。想象一位咖啡师,他可以同时处理多杯咖啡订单:他可能先磨一份豆子,然后去蒸汽牛奶,再回来冲泡第一份咖啡,接着处理第二份订单的磨豆。他不是同时做所有事情,而是在不同任务之间快速切换,以提高整体效率。
- 特点:任务之间通过时间片轮转、协作式调度或事件驱动进行切换。
- 目的:提高资源利用率和系统响应性。
- 实现方式:
- 多线程 (Multi-threading):在一个进程内创建多个执行流。线程共享进程的内存空间,切换开销小。在Python中,受限于GIL,多线程更适合I/O密集型任务。
- 多进程 (Multi-processing):创建多个独立的进程。每个进程有独立的内存空间,切换开销大。适合CPU密集型任务,不受GIL限制。
- 协程 (Coroutines/Async I/O):在单线程内通过协作式调度实现任务切换,通常用于I/O绑定任务,通过非阻塞I/O避免等待。
1.1.2 并行 (Parallelism)
并行是指在同一时刻,多个任务真正地同时执行。这通常需要多个独立的执行单元(如多核CPU)才能实现。回到咖啡师的例子,如果有两位咖啡师同时在工作,一位磨豆,另一位蒸汽牛奶,这就是并行。
- 特点:任务在物理上同时执行。
- 目的:缩短总任务完成时间,提高吞吐量。
- 实现方式:
- 多核CPU:操作系统将不同的进程或线程调度到不同的CPU核心上执行。
- 分布式系统:将任务分发到多台机器上并行处理。
并发与并行的关系:
- 有并发不一定有并行:例如,单核CPU上的多线程/多进程,或者协程,它们是并发的,但不是并行的。
- 有并行一定有并发:如果多个任务正在并行执行,那么它们自然也是并发处理的。
- 总结:并发是关于管理多个任务,而并行是关于同时执行多个任务。并发是并行的超集。
1.1.3 异步 (Asynchrony)
异步是指任务提交后,调用方不需要等待任务完成就可以继续执行自己的逻辑。当任务完成后,会通过某种机制(如回调、事件、Future对象)通知调用方或处理结果。它强调的是“非阻塞”的调用模式。
- 特点:非阻塞、事件驱动、通过回调或等待Future对象获取结果。
- 目的:提高程序的响应性,尤其是在等待I/O操作完成时。
- 实现方式:
- 回调函数:任务完成后调用预先注册的函数。
- Future/Promise模式:返回一个“未来结果”的占位符,可以稍后查询其状态或获取结果。
async/await
语法 (Pythonasyncio
):通过语言特性支持的协程实现非阻塞I/O。
concurrent.futures
库的核心正是采用了Future/Promise模式来管理并发任务的异步结果。
1.2 Python GIL (Global Interpreter Lock) 的深度剖析
理解Python的并发,特别是多线程,就无法绕开GIL——全局解释器锁。它是Python语言实现上的一个独特且极具影响力的设计选择。
1.2.1 GIL是什么?
GIL是CPython(官方Python解释器)在执行多线程代码时,为了保护内存管理而引入的一个互斥锁。它的作用是:在任意时刻,只允许一个线程执行Python字节码。这意味着即使在多核处理器上,一个Python进程中的多个线程也无法真正地并行执行Python代码。
注意:GIL是CPython解释器的特性,不是Python语言的特性。其他Python解释器(如Jython、IronPython)没有GIL,它们可以实现真正的多线程并行。但我们日常使用的绝大多数Python程序都运行在CPython上。
1.2.2 GIL 的工作原理
当一个Python线程需要执行字节码时,它必须首先获取GIL。一旦获取成功,它就可以执行Python代码,直到遇到以下情况之一,它会释放GIL:
- I/O操作:当线程执行文件读写、网络请求等I/O操作时,它会主动释放GIL,允许其他线程运行。这是因为I/O操作通常涉及到等待外部资源,这段等待时间CPU是空闲的,此时释放GIL可以提高整体效率。
- 达到时间片:CPython解释器内部会有一个机制,当一个线程持有GIL并执行了一段时间(通常是100个字节码指令或15毫秒,具体取决于版本和实现)后,它会强制释放GIL,即使它还没有完成当前任务,以便其他线程有机会运行。
- 主动释放:一些Python内置函数或第三方库在执行C语言代码时,如果知道这段C代码不会操作Python对象,它们可能会主动释放GIL,以允许其他Python线程运行。
1.2.3 GIL 对多线程编程的影响
- I/O密集型任务:对于I/O密集型任务(如网络爬取、文件处理、数据库操作),多线程是有效的。因为当一个线程等待I/O完成时,它会释放GIL,允许其他线程执行CPU计算或进行其自身的I/O操作。这样,CPU的等待时间被其他线程的工作所填充,提高了CPU的利用率和程序的响应性。
- CPU密集型任务:对于CPU密集型任务(如复杂的数值计算、图像处理),多线程是无效的,甚至可能适得其反。由于GIL的存在,多个线程无法真正并行地利用多核CPU进行计算。它们会不断地争抢GIL,导致线程切换的开销(上下文切换)反而抵消了并行带来的潜在收益,甚至可能比单线程执行更慢。
为了绕开GIL在CPU密集型任务中的限制,Python提供了多进程(multiprocessing
模块)。每个进程都有自己独立的Python解释器实例,因此也拥有自己独立的GIL。不同进程之间的GIL互不影响,从而实现了真正的并行执行。
1.3 为什么需要 concurrent.futures
?其在Python并发生态中的定位
在concurrent.futures
出现之前,Python的并发编程主要依赖于threading
和multiprocessing
模块。这两个模块提供了创建和管理线程/进程的基本API,但它们在使用上存在一些痛点:
1.3.1 传统多线程/多进程编程的痛点
- 手动管理线程/进程生命周期:开发者需要手动创建、启动、管理、等待线程/进程的完成,并在完成后进行清理。这增加了代码的复杂性和出错的风险。
- 结果收集困难:从子线程或子进程获取函数的返回值并不直接。通常需要通过
Queue
、共享变量(需要锁保护)或管道等机制来回传结果,这使得代码结构变得复杂。 - 异常处理复杂:子线程/子进程中发生的未捕获异常默认不会传播到主线程/主进程,开发者需要额外的机制(如try-except块结合Queue)来捕获并处理这些异常。
- 资源限制和开销:频繁地创建和销毁线程/进程会带来显著的开销。操作系统对可创建的线程/进程数量也有上限。
- 池化管理缺失:没有内置的线程池或进程池概念。开发者需要自己实现或使用第三方库来复用线程/进程,以减少创建和销毁的开销。
1.3.2 concurrent.futures
的诞生与核心理念
为了解决上述痛点,Python 3.2引入了concurrent.futures
库。它的核心理念是:将任务提交与结果获取解耦,并提供统一的高级接口来管理线程池和进程池。
concurrent.futures
库提供了一个高级抽象层,它定义了Executor
抽象基类,并提供了两种具体的实现:
ThreadPoolExecutor
:用于管理线程池,适用于I/O密集型任务。ProcessPoolExecutor
:用于管理进程池,适用于CPU密集型任务,能够绕开GIL实现真正的并行。
它通过引入Future
对象,极大地简化了异步结果的获取和异常处理。
concurrent.futures
在Python并发生态系统中的定位:
- 高级抽象:它是对
threading
和multiprocessing
模块的封装和抽象,提供了更简洁、更易用的API。 - 统一接口:无论底层是线程还是进程,
concurrent.futures
都提供了一致的Executor
和Future
接口,降低了学习成本和代码迁移的难度。 - 简化任务管理:通过线程池和进程池,它自动化了线程/进程的生命周期管理,包括创建、复用和销毁。
- 异步结果管理:
Future
对象是其核心,使得异步任务的结果获取、状态查询和回调注册变得直观和简单。 - 适用于广泛场景:无论是I/O绑定还是CPU绑定任务,
concurrent.futures
都能提供合适的解决方案。
第二章:Future
对象:并发结果的占位符与异步模型的核心
在concurrent.futures
库中,Future
对象是理解和掌握其强大功能的核心。它不仅仅是一个简单的返回值容器,更是一个精心设计的抽象,代表了一个尚未完成的异步操作的结果。在本章中,我们将以前所未有的深度,剖析Future
对象的内部机制、生命周期、状态管理,以及如何利用它高效地处理并发任务的结果和异常。
2.1 Future
对象的本质与生命周期
Future
(未来)这个名字本身就暗示了其作用:它是一个承诺,在未来的某个时间点,它将持有某个操作的结果或异常。当你向Executor
(无论是ThreadPoolExecutor
还是ProcessPoolExecutor
)提交一个任务时,Executor
不会立即返回任务的实际结果,而是立即返回一个Future
对象。这个Future
对象就像一张“收据”或“占位符”,你可以在稍后通过这张“收据”来查询任务的状态,或者最终获取到任务的真实结果。
2.1.1 Future
对象的初始状态与转变
一个Future
对象在其生命周期中会经历多种状态。理解这些状态及其转换是正确使用Future
的关键。
Future
对象的核心状态通常包括:
- PENDING (挂起):任务已提交到
Executor
,但尚未开始执行。它可能正在等待线程池或进程池中的可用工作者。 - RUNNING (运行中):任务已被一个工作者(线程或进程)选中,正在执行中。
- CANCELLED (已取消):任务在执行前被明确取消。如果任务已经开始运行,则无法被取消。
- FINISHED (已完成):任务已经完成执行。完成状态又分为两种子状态:
- SUCCEEDED (成功):任务执行成功,并返回了一个结果。
- FAILED (失败):任务执行过程中抛出了一个未捕获的异常。
这些状态之间的转换是单向的:
PENDING
->RUNNING
(任务开始执行)PENDING
->CANCELLED
(任务在执行前被取消)RUNNING
->FINISHED
(任务执行完毕,无论是成功还是失败)CANCELLED
或FINISHED
都是最终状态,一旦进入这些状态,Future
对象的状态就不会再改变。
上述Mermaid图描述了Future对象的状态转换。A表示PENDING(挂起),B表示RUNNING(运行中),C表示CANCELLED(已取消),D表示FINISHED(已完成),E表示SUCCEEDED(成功),F表示FAILED(失败),G表示Terminal(终结状态)。从PENDING可以转为RUNNING或CANCELLED。从RUNNING只能转为FINISHED。FINISHED又分为SUCCEEDED和FAILED两种子状态。CANCELLED、SUCCEEDED、FAILED都是最终状态,一旦达到则不再变化。
内部实现简述:
Future
对象在内部通常会维护一个状态变量,并通过锁(如threading.Lock
)来保护其状态的原子性更新。当任务在工作者中执行完毕(无论是正常返回还是抛出异常),工作者会将结果或异常设置到对应的Future
对象中,并更新其状态为FINISHED
。同时,任何阻塞在Future
对象上的等待操作(如result()
或exception()
)都将被唤醒。
2.2 Future
对象的基本方法
Future
对象提供了一系列直观的方法来查询其状态、获取结果或异常,以及注册回调函数。
2.2.1 done()
- 查询任务是否完成
done()
方法用于检查Future
所代表的任务是否已经完成。如果任务已完成(即状态为CANCELLED
或FINISHED
),则返回True
;否则返回False
。
import concurrent.futures
import time
def simulate_task(duration):
# 模拟一个耗时任务
print(f"任务开始执行,预计耗时 {
duration} 秒...") # 打印任务开始执行的信息和预计耗时
time.sleep(duration) # 让当前线程/进程休眠指定秒数,模拟任务执行时间
print(f"任务执行完毕,耗时 {
duration} 秒。") # 打印任务执行完毕的信息和实际耗时
return f"任务 {
duration} 秒的结果" # 返回一个字符串作为任务的结果
# 使用ThreadPoolExecutor创建线程池
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # 创建一个线程池,最大工作线程数为3
# 提交一个耗时3秒的任务
future_long = executor.submit(simulate_task, 3) # 向线程池提交simulate_task函数,参数为3秒,并获取返回的Future对象
# 提交一个耗时1秒的任务
future_short = executor.submit(simulate_task, 1) # 向线程池提交simulate_task函数,参数为1秒,并获取返回的Future对象
# 循环检查两个Future对象的状态
while not future_long.done() or not future_short.done(): # 循环条件:只要其中任何一个Future对象尚未完成,就继续循环
print("等待任务完成...") # 打印提示信息,表示正在等待任务完成
time.sleep(0.5) # 暂停0.5秒,避免CPU空转,同时给任务执行留出时间
if future_short.done(): # 检查短耗时任务是否完成
print(f"短耗时任务已完成: {
future_short.result()}") # 如果短耗时任务完成,打印其已完成的状态和获取结果
if future_long.done(): # 检查长耗时任务是否完成
print(f"长耗时任务已完成: {
future_long.result()}") # 如果长耗时任务完成,打印其已完成的状态和获取结果
print("所有任务都已完成。") # 打印所有任务都已完成的最终信息
# 再次尝试获取结果,此时不会阻塞
print(f"最终获取长耗时任务结果: {
future_long.result()}") # 再次获取长耗时任务的结果,此时任务已完成,不会阻塞
print(f"最终获取短耗时任务结果: {
future_short.result()}") # 再次获取短耗时任务的结果,此时任务已完成,不会阻塞
代码解析:
- 我们定义了
simulate_task
函数模拟耗时操作。 - 通过
executor.submit()
提交任务后,会立即返回Future
对象。 - 我们使用
while not future.done()
循环来非阻塞地检查任务状态。当done()
返回True
时,表示任务已完成,可以安全地获取结果。 - 注意:在
done()
返回True
之前调用result()
会阻塞。本例中,我们在done()
检查通过后才调用result()
,以演示其非阻塞特性。
2.2.2 running()
- 查询任务是否正在运行
running()
方法用于检查Future
所代表的任务是否正在执行中。如果任务状态为RUNNING
,则返回True
;否则返回False
。
import concurrent.futures
import time
def busy_task(task_id, duration):
# 一个模拟耗时且需要CPU工作的任务
print(f"任务 {
task_id} 开始运行,预计耗时 {
duration} 秒...") # 打印任务ID和开始运行信息
start_time = time.time() # 记录任务开始时间
while time.time() - start_time < duration: # 循环,模拟CPU密集型计算,直到达到指定持续时间
# 简单计算,保持CPU忙碌
_ = [i*i for i in range(10000)] # 执行一个列表推导式,模拟CPU计算,不关心结果,只为了消耗CPU时间
pass # 占位符,无实际操作
print(f"任务 {
task_id} 运行结束。") # 打印任务ID和运行结束信息
return f"任务 {
task_id} 完成!" # 返回任务完成的字符串结果
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: # 创建一个线程池,最大工作线程数为2
# 提交三个任务,前两个会立即运行,第三个可能挂起
future1 = executor.submit(busy_task, 1, 4) # 提交第一个任务,ID为1,耗时4秒
future2 = executor.submit(busy_task, 2, 2) # 提交第二个任务,ID为2,耗时2秒
future3 = executor.submit(busy_task, 3, 3) # 提交第三个任务,ID为3,耗时3秒
print("\n--- 初始状态检查 ---") # 打印初始状态检查的标题
print(f"Future 1 正在运行? {
future1.running()}") # 检查并打印Future 1是否正在运行
print(f"Future 2 正在运行? {
future2.running()}") # 检查并打印Future 2是否正在运行
print(f"Future 3 正在运行? {
future3.running()}") # 检查并打印Future 3是否正在运行
print("理论上,由于max_workers=2,Future 3 应该处于PENDING或刚刚RUNNING的状态,取决于调度速度。\n") # 解释Future 3的状态可能原因
time.sleep(1) # 等待1秒,让任务有机会开始运行
print("\n--- 1秒后状态检查 ---") # 打印1秒后状态检查的标题
print(f"Future 1 正在运行? {
future1.running()}") # 检查并打印Future 1是否正在运行
print(f"Future 2 正在运行? {
future2.running()}") # 检查并打印Future 2是否正在运行
print(f"Future 3 正在运行? {
future3.running()}") # 检查并打印Future 3是否正在运行
print("此时 Future 1 和 2 应该在运行,Future 3 可能还在PENDING或刚开始运行。\n") # 解释此时Future 1、2、3的状态
time.sleep(2) # 再等待2秒,即总共等待3秒
print("\n--- 3秒后状态检查 ---") # 打印3秒后状态检查的标题
print(f"Future 1 正在运行? {
future1.running()}") # 检查并打印Future 1是否正在运行
print(f"Future 2 正在运行? {
future2.running()}") # 检查并打印Future 2是否正在运行
print(f"Future 3 正在运行? {
future3.running()}") # 检查并打印Future 3是否正在运行
print("Future 2 应该已经完成,Future 1 仍在运行,Future 3 可能已开始运行。\n") # 解释此时Future 1、2、3的状态
# 等待所有任务完成
print("\n--- 等待所有任务完成并获取结果 ---") # 打印等待所有任务完成并获取结果的标题
for future in [future1, future2, future3]: # 遍历所有Future对象
result = future.result() # 获取Future对象的结果(会阻塞直到任务完成)
print(f"获取到结果: {
result}, 任务是否仍在运行? {
future.running()}, 是否已完成? {
future.done()}") # 打印获取到的结果,并检查任务是否仍在运行和是否已完成
代码解析:
- 我们创建了一个
ThreadPoolExecutor
,最大工作线程数为2。 - 提交了3个任务,由于线程池限制,第三个任务可能需要等待。
- 我们通过不同时间点的
running()
和done()
方法来观察任务状态的变化。 running()
在任务执行过程中返回True
,任务结束后返回False
。done()
在任务完成(无论成功、失败还是取消)后返回True
。- 这个例子很好地展示了任务从
PENDING
到RUNNING
再到FINISHED
(隐含SUCCEEDED
)的状态流转。
2.2.3 cancelled()
- 查询任务是否被取消
cancelled()
方法用于检查Future
所代表的任务是否在开始执行前被取消。如果任务被成功取消,则返回True
;否则返回False
。
import concurrent.futures
import time
def cancellable_task(task_id, duration):
# 模拟一个可被取消的任务
print(f"任务 {
task_id} 尝试开始运行...") # 打印任务ID和尝试开始运行的信息
time.sleep(duration / 2) # 模拟任务执行一半时间
print(f"任务 {
task_id} 运行了一半。") # 打印任务运行了一半的信息
time.sleep(duration / 2) # 模拟任务执行剩余一半时间
print(f"任务 {
task_id} 正常完成。") # 打印任务正常完成的信息
return f"任务 {
task_id} 的成功结果" # 返回任务成功的结果
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: # 创建一个线程池,最大工作线程数为2
print("--- 尝试取消未开始的任务 ---") # 打印尝试取消未开始的任务的标题
future_to_cancel = executor.submit(cancellable_task, "A", 5) # 提交一个耗时5秒的任务A
future_normal = executor.submit(cancellable_task, "B", 1) # 提交一个耗时1秒的任务B(这个任务会很快完成,用于对比)
future_another_cancel = executor.submit(cancellable_task, "C", 6) # 提交另一个耗时6秒的任务C,也尝试取消
time.sleep(0.1) # 短暂等待,让executor有机会调度任务
# 尝试取消 future_to_cancel
print(f"尝试取消任务 A (is_pending={
future_to_cancel.running() or future_to_cancel.done()}):") # 打印尝试取消任务A的信息,并检查其是否已运行或已完成
was_cancelled_a = future_to_cancel.cancel() # 尝试取消任务A
print(f"任务 A 取消成功? {
was_cancelled_a}") # 打印任务A是否成功取消的结果
print(f"任务 A 状态: cancelled={
future_to_cancel.cancelled()}, done={
future_to_cancel.done()}, running={
future_to_cancel.running()}") # 打印任务A的当前状态
# 尝试取消 future_another_cancel
# 这个任务可能已经在运行,或者还在PENDING
print(f"\n尝试取消任务 C (is_pending={
future_another_cancel.running() or future_another_cancel.done()}):") # 打印尝试取消任务C的信息,并检查其是否已运行或已完成
was_cancelled_c = future_another_cancel.cancel() # 尝试取消任务C
print(f"任务 C 取消成功? {
was_cancelled_c}") # 打印任务C是否成功取消的结果
print(f"任务 C 状态: cancelled={
future_another_cancel.cancelled()}, done={
future_another_cancel.done()}, running={
future_another_cancel.running()}") # 打印任务C的当前状态
# 等待一些时间,让任务 B 完成,任务 A 和 C 如果没取消成功就继续运行
time.sleep(2)
print("\n--- 2秒后状态检查及结果获取 ---") # 打印2秒后状态检查及结果获取的标题
for i, fut in enumerate([future_to_cancel, future_normal, future_another_cancel]): # 遍历所有Future对象
try: # 尝试获取任务结果
result = fut.result(timeout=1) # 获取任务结果,设置超时1秒
print(f"任务 {
chr(65+i)} 结果: {
result}") # 打印任务结果
except concurrent.futures.CancelledError: # 捕获任务被取消的异常
print(f"任务 {
chr(65+i)} 已被取消 (CancelledError).") # 打印任务已被取消的信息
except concurrent.futures.TimeoutError: # 捕获获取结果超时的异常
print(f"任务 {
chr(65+i)} 获取结果超时,可能仍在运行或等待。") # 打印获取结果超时信息
except Exception as e: # 捕获其他异常
print(f"任务 {
chr(65+i)} 发生异常: {
e}") # 打印任务发生的其他异常
print(f"任务 {
chr(65+i)} 最终状态: cancelled={
fut.cancelled()}, done={
fut.done()}, running={
fut.running()}") # 打印任务的最终状态
代码解析:
future.cancel()
方法尝试取消任务。- 如果任务尚未开始执行(即状态为
PENDING
),cancel()
将返回True
并成功取消任务,Future
的状态变为CANCELLED
。此时,尝试调用result()
或exception()
将抛出CancelledError
。 - 如果任务已经开始执行(状态为
RUNNING
),cancel()
将返回False
,任务会继续执行直到完成。 - 如果任务已经完成(状态为
FINISHED
),cancel()
也将返回False
。 - 这个例子展示了在不同阶段尝试取消任务的效果,以及如何捕获
CancelledError
。
2.2.4 result(timeout=None)
- 获取任务结果
result()
方法是Future
对象最常用的方法之一,用于获取任务的返回值。
- 如果任务尚未完成,调用此方法会阻塞当前线程/进程,直到任务完成并返回结果。
- 如果任务执行成功,它将返回任务函数的返回值。
- 如果任务执行过程中抛出了异常,调用
result()
会重新抛出该异常。 - 如果任务被取消,它会抛出
CancelledError
。 timeout
参数可以指定等待结果的最长时间(秒)。如果在此时间内任务未完成,将抛出TimeoutError
。
import concurrent.futures
import time
import random
def data_processing_task(data_chunk_id, processing_time):
# 模拟数据处理任务,可能成功,也可能失败
print(f"开始处理数据块 {
data_chunk_id},预计耗时 {
processing_time:.2f} 秒...") # 打印开始处理数据块信息
time.sleep(processing_time) # 模拟数据处理时间
if random.random() < 0.2: # 20%的概率模拟处理失败
raise ValueError(f"数据块 {
data_chunk_id} 处理失败:模拟错误。") # 抛出ValueError模拟处理失败
print(f"数据块 {
data_chunk_id} 处理成功。") # 打印数据块处理成功信息
return f"ProcessedData_{
data_chunk_id}_Result" # 返回处理成功的结果
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: # 创建一个线程池,最大工作线程数为5
futures = [] # 创建一个空列表用于存储Future对象
print("--- 提交多个数据处理任务 ---") # 打印提交多个数据处理任务的标题
for i in range(1, 8): # 提交7个任务
delay = random.uniform(0.5, 3.0) # 随机生成0.5到3.0秒的处理时间
future = executor.submit(data_processing_task, i, delay) # 提交数据处理任务
futures.append(future) # 将Future对象添加到列表中
print(f"任务 {
i} 已提交,预计耗时 {
delay:.2f} 秒。") # 打印任务提交信息
print("\n--- 逐个获取任务结果 (带超时处理和异常捕获) ---") # 打印逐个获取任务结果的标题
for i, future in enumerate(futures): # 遍历Future对象列表
task_id = i + 1 # 计算任务ID
try: # 尝试获取结果
print(f"尝试获取任务 {
task_id} 的结果...") # 打印尝试获取任务结果的信息
# 尝试在2秒内获取结果
result = future.result(timeout=2.0) # 获取任务结果,设置超时2秒
print(f"✓ 任务 {
task_id} 成功完成,结果: {
result}") # 打印任务成功完成的信息和结果
except concurrent.futures.TimeoutError: # 捕获超时异常
print(f"✗ 任务 {
task_id} 超时未完成,可能仍在处理中。") # 打印任务超时未完成信息
except ValueError as e: # 捕获模拟的ValueError
print(f"✗ 任务 {
task_id} 处理失败:{
e}") # 打印任务处理失败和错误信息
except Exception as e: # 捕获其他所有异常
print(f"✗ 任务 {
task_id} 发生未知错误:{
type(e).__name__}: {
e}") # 打印任务发生的未知错误类型和信息
finally: # 无论是否发生异常,都会执行
print(f"任务 {
task_id} 当前状态: done={
future.done()}, cancelled={
future.cancelled()}, running={
future.running()}") # 打印任务的最终状态
print("\n所有已提交的任务都已尝试获取结果。") # 打印所有已提交的任务都已尝试获取结果的提示
代码解析:
data_processing_task
函数模拟了数据处理,并有一定概率抛出ValueError
。- 我们通过
executor.submit()
提交多个任务,并将返回的Future
对象存储在列表中。 - 在循环中,我们使用
future.result(timeout=2.0)
尝试获取每个任务的结果。 try...except
块是至关重要的,它演示了如何捕获三种可能的情况:TimeoutError
:当在指定timeout
时间内任务未完成时。ValueError
:当任务函数本身抛出我们预期的业务逻辑异常时。Exception
:捕获所有其他意外异常。
- 这个例子强调了
result()
方法的阻塞特性、超时机制以及异常传播。
2.2.5 exception(timeout=None)
- 获取任务异常
exception()
方法用于获取任务执行过程中抛出的异常。
- 如果任务尚未完成,调用此方法会阻塞当前线程/进程,直到任务完成。
- 如果任务成功完成,它将返回
None
。 - 如果任务执行过程中抛出了异常,它将返回该异常对象。
- 如果任务被取消,它会抛出
CancelledError
。 timeout
参数与result()
方法类似,用于设置等待异常的最长时间。
import concurrent.futures
import time
import random
def potentially_failing_task(task_id, chance_of_failure):
# 一个可能失败的任务
print(f"任务 {
task_id} 开始执行...") # 打印任务ID和开始执行信息
time.sleep(random.uniform(0.5, 1.5)) # 模拟随机耗时
if random.random() < chance_of_failure: # 根据失败概率判断是否抛出异常
error_type = random.choice([ValueError, TypeError, ZeroDivisionError]) # 随机选择一个异常类型
raise error_type(f"任务 {
task_id} 失败,类型为 {
error_type.__name__}.") # 抛出随机选择的异常
print(f"任务 {
task_id} 成功完成。") # 打印任务成功完成信息
return f"任务 {
task_id} 的结果" # 返回任务成功的结果
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # 创建一个线程池,最大工作线程数为3
futures = [] # 创建一个空列表存储Future对象
print("--- 提交一批可能失败的任务 ---") # 打印提交任务的标题
for i in range(1, 6): # 提交5个任务
# 任务1和任务5失败概率高,其他失败概率低
failure_prob = 0.8 if i in [1, 5] else 0.1 # 设置任务的失败概率
future = executor.submit(potentially_failing_task, i, failure_prob) # 提交任务
futures.append(future) # 将Future对象添加到列表中
print(f"任务 {
i} 已提交,失败概率: {
failure_prob*100:.0f}%.") # 打印任务提交信息和失败概率
print("\n--- 逐个检查任务的异常情况 ---") # 打印检查任务异常情况的标题
for i, future in enumerate(futures): # 遍历Future对象列表
task_id = i + 1 # 计算任务ID
try: # 尝试获取异常
# 等待任务完成,最多等待5秒来获取异常
exc = future.exception(timeout=5) # 获取任务的异常,设置超时5秒
if exc: # 如果存在异常
print(f"✗ 任务 {
task_id} 抛出了异常: {
type(exc).__name__}: {
exc}") # 打印任务抛出的异常类型和信息
else: # 如果没有异常
print(f"✓ 任务 {
task_id} 成功完成,没有异常。结果: {
future.result()}") # 打印任务成功完成信息和结果
except concurrent.futures.TimeoutError: # 捕获超时异常
print(f"✗ 任务 {
task_id} 等待异常超时,可能仍在运行或等待。") # 打印等待异常超时信息
except concurrent.futures.CancelledError: # 捕获任务被取消的异常
print(f"✗ 任务 {
task_id} 已被取消。") # 打印任务已被取消信息
finally: # 无论是否发生异常,都会执行
print(f"任务 {
task_id} 最终状态: done={
future.done()}, cancelled={
future.cancelled()}, running={
future.running()}") # 打印任务的最终状态
代码解析:
potentially_failing_task
函数根据chance_of_failure
随机抛出不同类型的异常。- 我们使用
future.exception(timeout=5)
尝试获取每个任务的异常。 - 如果
exception()
返回一个异常对象,说明任务失败;如果返回None
,说明任务成功完成。 - 与
result()
类似,exception()
也会阻塞,并支持timeout
参数,同样会传播CancelledError
。 - 这个例子展示了如何通过
exception()
方法来集中处理并发任务中的错误,而无需在调用result()
时进行大量的try-except
。
2.2.6 add_done_callback(fn)
- 注册完成回调
add_done_callback()
方法允许你在Future
对象完成时(无论成功、失败或取消)注册一个或多个回调函数。
- 回调函数只接受一个参数:已完成的
Future
对象本身。 - 如果
Future
对象在注册回调时已经完成,回调会立即执行。 - 回调函数通常在
Future
所关联的任务执行线程/进程中执行,但具体实现取决于Executor
。对于ThreadPoolExecutor
,回调通常在处理该Future
的工作线程中执行;对于ProcessPoolExecutor
,回调通常在提交任务的那个主进程中执行,或者在一个独立的内部线程中执行。 - 重要提示:在回调函数中执行耗时的操作可能会阻塞工作者线程/进程,影响池的效率。因此,回调函数应该轻量级,或者将耗时操作再次提交到另一个
Executor
中。
import concurrent.futures
import time
import random
def worker_function(task_id, delay, should_fail=False):
# 模拟工作函数
print(f"任务 {
task_id} 开始 (线程ID: {
threading.get_ident()})...") # 打印任务ID和当前线程ID
time.sleep(delay) # 模拟耗时
if should_fail: # 如果should_fail为True,则模拟失败
raise ValueError(f"任务 {
task_id} 模拟失败!") # 抛出ValueError
print(f"任务 {
task_id} 完成 (线程ID: {
threading.get_ident()}).") # 打印任务ID和完成信息
return f"数据 {
task_id} 已处理" # 返回任务结果
def result_callback(future):
# 这是一个回调函数,当Future完成时会被调用
print(f"\n--- 回调函数执行开始 (当前线程ID: {
threading.get_ident()}) ---") # 打印回调函数开始执行信息和当前线程ID
try: # 尝试获取结果
result = future.result() # 获取Future的结果,如果任务失败会抛出异常
print(f"回调:任务成功完成,结果: {
result}") # 打印任务成功完成信息和结果
except concurrent.futures.CancelledError: # 捕获任务被取消的异常
print("回调:任务被取消了。") # 打印任务被取消信息
except Exception as e: # 捕获其他异常
print(f"回调:任务发生异常: {
type(e).__name__}: {
e}") # 打印任务发生的异常类型和信息
print("--- 回调函数执行结束 ---\n") # 打印回调函数执行结束信息
import threading # 导入threading模块以获取线程ID
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # 创建一个线程池,最大工作线程数为3
print("主线程开始提交任务 (主线程ID:", threading.get_ident(), ")") # 打印主线程开始提交任务的信息和主线程ID
# 提交一个成功任务
future1 = executor.submit(worker_function, "A", 2, False) # 提交任务A,不失败
future1.add_done_callback(result_callback) # 为任务A注册回调函数
print("任务 A 已提交,并注册回调。") # 打印任务A提交信息
# 提交一个失败任务
future2 = executor.submit(worker_function, "B", 1, True) # 提交任务B,模拟失败
future2.add_done_callback(result_callback) # 为任务B注册回调函数
print("任务 B 已提交,并注册回调。") # 打印任务B提交信息
# 提交一个稍后取消的任务
future3 = executor.submit(worker_function, "C", 5, False) # 提交任务C,不失败
future3.add_done_callback(result_callback) # 为任务C注册回调函数
print("任务 C 已提交,并注册回调。") # 打印任务C提交信息
# 立即尝试取消任务C
time.sleep(0.1) # 短暂等待,确保任务C有机会被Executor接收
if future3.cancel(): # 尝试取消任务C
print("主线程:成功取消任务 C。") # 如果取消成功,打印信息
else:
print("主线程:未能取消任务 C,可能已开始运行。") # 如果取消失败,打印信息
# 提交一个等待时间更长的任务,其回调会在稍后触发
future4 = executor.submit(worker_function, "D", 4, False) # 提交任务D,不失败
future4.add_done_callback(result_callback) # 为任务D注册回调函数
print("任务 D 已提交,并注册回调。") # 打印任务D提交信息
print("\n主线程继续执行其他逻辑...") # 打印主线程继续执行其他逻辑的信息
# 主线程不阻塞,等待回调自行触发
time.sleep(7) # 模拟主线程的其他工作,并等待所有任务和回调有时间完成
print("\n主线程执行完毕。") # 打印主线程执行完毕信息
代码解析:
- 我们定义了一个
result_callback
函数,它接收一个Future
对象作为参数,并在其中安全地获取结果或捕获异常。 - 通过
future.add_done_callback(callback_function)
为每个提交的Future
对象注册了回调。 - 即使主线程不主动调用
result()
或exception()
阻塞等待,当任务完成时,注册的回调函数也会自动执行。 - 本例演示了回调函数如何处理成功、失败和被取消的任务。
- 请注意,打印出的线程ID会显示回调函数通常在工作线程中执行(对于
ThreadPoolExecutor
)。
2.2.7 set_result(result)
/ set_exception(exception)
(内部方法,通常不直接使用)
这两个方法是Future
对象内部使用的,用于由执行任务的工作者(线程或进程)设置任务的最终结果或异常。作为concurrent.futures
的用户,你通常不需要直接调用它们,因为Executor
会在后台为你处理这些。了解它们的存在有助于理解Future
的工作机制。
set_result(result)
:将Future
的状态设置为FINISHED
,并将传入的result
作为任务的返回值。所有等待该Future
结果的调用(如result()
)都将被唤醒并返回此结果。set_exception(exception)
:将Future
的状态设置为FINISHED
,并将传入的exception
作为任务的异常。所有等待该Future
结果的调用(如result()
)都将被唤醒并重新抛出此异常;调用exception()
则会返回此异常。
这两个方法在设计自定义Executor
或高级并发结构时可能会用到,但在日常使用ThreadPoolExecutor
或ProcessPoolExecutor
时,它们是透明的。
2.3 组合 Future
对象:concurrent.futures.as_completed()
和 concurrent.futures.wait()
当提交多个任务时,我们通常不希望按照提交的顺序来获取结果,因为耗时短的任务可能先完成。concurrent.futures
提供了两个强大的函数来管理多个Future
对象:as_completed()
和wait()
,它们允许我们更灵活、高效地处理并发任务的结果。
2.3.1 concurrent.futures.as_completed(futures, timeout=None)
- 按完成顺序获取结果
as_completed()
函数接收一个Future
对象的可迭代对象(如列表或集合),并返回一个迭代器。这个迭代器会按任务完成的顺序产生Future
对象。这意味着你可以立即处理那些已经完成的任务,而无需等待所有任务都完成。这对于需要实时处理结果或处理大量并发任务的场景非常有用。
futures
:一个包含Future
对象的集合(例如由executor.submit()
返回的Future
列表)。timeout
:可选参数,指定等待下一个Future
完成的最长时间。如果在此时间内没有Future
完成,将抛出concurrent.futures.TimeoutError
。
import concurrent.futures
import time
import random
def website_fetcher(url):
# 模拟从URL获取内容,耗时随机
print(f"开始抓取: {
url}...") # 打印开始抓取URL的信息
fetch_time = random.uniform(1, 4) # 随机生成1到4秒的抓取时间
time.sleep(fetch_time) # 模拟网络延迟
content_length = random.randint(1000, 10000) # 随机生成内容长度
print(f"完成抓取: {
url}, 耗时 {
fetch_time:.2f} 秒, 内容长度 {
content_length}.") # 打印完成抓取信息、耗时和内容长度
return f"Content for {
url} (Length: {
content_length})" # 返回模拟的内容
urls = [
"http://example.com/page1", "http://example.com/page2",
"http://example.com/page3", "http://example.com/page4",
"http://example.com/page5", "http://example.com/page6",
"http://example.com/page7", "http://example.com/page8"
] # 定义一个URL列表,模拟待抓取的网页
print("--- 使用 ThreadPoolExecutor 并 as_completed 按完成顺序处理 ---") # 打印标题
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: # 创建一个线程池,最大工作线程数为5
# 提交所有URL抓取任务,并收集Future对象
submitted_futures = {
executor.submit(website_fetcher, url): url for url in urls} # 提交任务并创建一个字典,键是Future对象,值是对应的URL
print(f"已提交 {
len(submitted_futures)} 个任务。") # 打印已提交任务的数量
print("\n按完成顺序处理结果:") # 打印按完成顺序处理结果的提示
# as_completed 会迭代地返回已完成的Future对象
# 注意:as_completed 默认没有超时,它会一直等待直到所有future都完成
for future in concurrent.futures.as_completed(submitted_futures, timeout=10): # 迭代器会按完成顺序返回Future对象,设置总超时10秒
url_associated = submitted_futures[future] # 从字典中根据Future对象获取原始的URL
try: # 尝试获取Future的结果
result = future.result() # 获取Future的结果
print(f"✔ 成功处理: {
url_associated} -> {
result[:50]}...") # 打印成功处理的URL和结果(截取前50字符)
except Exception as exc: # 捕获可能发生的异常
print(f"✖ 任务 {
url_associated} 发生异常: {
exc}") # 打印发生异常的URL和异常信息
finally: # 无论成功或失败,都打印当前Future的状态
print(f" Future for {
url_associated} 状态: done={
future.done()}, cancelled={
future.cancelled()}") # 打印当前Future的状态
print("\n所有任务已处理完毕(或达到 as_completed 的超时)。") # 打印所有任务已处理完毕的提示
# 演示 as_completed 的 timeout 参数
print("\n--- 演示 as_completed 的 timeout 参数 ---") # 打印演示as_completed的timeout参数的标题
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # 创建一个线程池,最大工作线程数为3
futures_for_timeout = [] # 创建一个空列表用于存储Future对象
for i in range(1, 5): # 提交4个任务
delay = i * 1.5 # 设置任务的延迟时间,依次为1.5, 3.0, 4.5, 6.0秒
futures_for_timeout.append(executor.submit(website_fetcher, f"http://slow.com/page{
i} (delay {
delay}s)")) # 提交任务并添加到列表
print(f"提交任务 (延迟 {
delay}s)") # 打印提交任务的信息
try: # 尝试迭代Future对象
# 设置as_completed的超时为4秒
# 意味着如果4秒内没有新的Future完成,就会抛出TimeoutError
for i, future in enumerate(concurrent.futures.as_completed(futures_for_timeout, timeout=4)): # 迭代Future对象,设置as_completed的超时为4秒
result = future.result() # 获取Future结果
print(f"获取到第 {
i+1} 个结果: {
result}") # 打印获取到的结果
except concurrent.futures.TimeoutError: # 捕获超时异常
print("\nas_completed 在等待下一个结果时超时了!") # 打印超时信息
print("未完成的任务列表:") # 打印未完成任务列表的标题
for future in futures_for_timeout: # 遍历所有Future对象
if not future.done(): # 如果Future尚未完成
print(f" - {
future._fn_args[0]} (仍在等待)") # 打印未完成任务的URL
except Exception as e: # 捕获其他异常
print(f"发生其他错误: {
e}") # 打印其他错误信息
print("\n所有任务完成或超时处理结束。") # 打印所有任务完成或超时处理结束的提示
代码解析:
- 我们模拟了一组URL抓取任务,每个任务耗时随机。
- 通过
executor.submit()
提交任务,并将Future
对象及其对应的URL存储在字典中。 concurrent.futures.as_completed(submitted_futures)
返回一个迭代器,当我们遍历这个迭代器时,每当有一个任务完成,它对应的Future
对象就会被产出。- 这样我们就可以立即处理已完成的任务,而不需要等待所有任务都完成,从而提高了程序的响应性。
- 第二个示例展示了
as_completed
的timeout
参数。它指定了在两次连续的Future
产出之间等待的最长时间。如果在此时间内没有新的Future
完成,就会抛出TimeoutError
。这与future.result()
的timeout
是不同的,future.result()
是等待单个Future
完成,而as_completed
的timeout
是等待下一个Future
完成。
2.3.2 concurrent.futures.wait(futures, timeout=None, return_when=ALL_COMPLETED)
- 等待一组任务完成
wait()
函数用于等待给定的Future
对象集合中的某些或所有任务完成。它会阻塞当前线程/进程,直到满足return_when
参数指定的条件,或者达到timeout
。
futures
:一个包含Future
对象的集合。timeout
:可选参数,指定等待的总时间。如果在此时间内没有满足return_when
的条件,函数会立即返回。return_when
:一个字符串,定义了何时返回。有三个预定义的值:ALL_COMPLETED
(默认值):当所有Future
都完成时返回。FIRST_COMPLETED
:当任意一个Future
完成时返回。FIRST_EXCEPTION
:当任意一个Future
抛出异常时返回。
wait()
函数返回一个具名元组DoneAndNotDoneFutures
,包含两个集合:done
:已完成(包括成功、失败或取消)的Future
对象的集合。not_done
:未完成的Future
对象的集合。
import concurrent.futures
import time
import random
def complex_calc(task_id, duration, should_fail=False):
# 模拟复杂的计算任务
print(f"任务 {
task_id} 开始计算,预计耗时 {
duration:.2f} 秒...") # 打印任务ID和开始计算信息
time.sleep(duration) # 模拟计算时间
if should_fail: # 如果should_fail为True,则模拟失败
raise RuntimeError(f"任务 {
task_id} 计算失败!") # 抛出RuntimeError
print(f"任务 {
task_id} 计算完成。") # 打印任务ID和计算完成信息
return f"计算结果 for {
task_id}" # 返回计算结果
print("--- 演示 wait(return_when=FIRST_COMPLETED) ---") # 打印演示wait(return_when=FIRST_COMPLETED)的标题
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor: # 创建一个进程池,最大工作进程数为4
futures_first = [] # 创建一个空列表用于存储Future对象
# 提交一批不同耗时的任务
futures_first.append(executor.submit(complex_calc, "A", 3.0)) # 提交任务A,耗时3秒
futures_first.append(executor.submit(complex_calc, "B", 1.0)) # 提交任务B,耗时1秒 (最快完成)
futures_first.append(executor.submit(complex_calc, "C", 5.0)) # 提交任务C,耗时5秒
futures_first.append(executor.submit(complex_calc, "D", 2.0)) # 提交任务D,耗时2秒
print("等待第一个任务完成...") # 打印等待第一个任务完成的提示
# 等待第一个任务完成
done, not_done = concurrent.futures.wait(futures_first, return_when=concurrent.futures.FIRST_COMPLETED) # 等待第一个Future对象完成
print(f"\n第一个任务已完成。已完成数量: {
len(done)}, 未完成数量: {
len(not_done)}") # 打印已完成和未完成任务的数量
for f in done: # 遍历已完成的Future对象
try: # 尝试获取结果
print(f"完成任务结果: {
f.result()}") # 打印已完成任务的结果
except Exception as e: # 捕获异常
print(f"完成任务发生异常: {
e}") # 打印完成任务发生的异常
print("未完成的任务列表 (这些任务可能仍在运行):") # 打印未完成任务列表的标题
for f in not_done: # 遍历未完成的Future对象
print(f" - {
f._fn_args[0]} (状态: running={
f.running()}, done={
f.done()})") # 打印未完成任务的ID和状态
# 清理剩余任务 (可选,但通常在实际应用中需要处理)
# 对于ProcessPoolExecutor,最好等待所有任务完成或手动关停
for f in not_done: # 遍历未完成的Future对象
# 在这里可以决定是取消它们,还是继续等待
# 为了演示完整性,我们继续等待它们,但在实际应用中可能选择取消或设置更长的超时
f.result() # 阻塞等待剩余任务完成
print("所有任务已最终完成。") # 打印所有任务已最终完成的提示
print("\n--- 演示 wait(return_when=ALL_COMPLETED) 并带超时 ---") # 打印演示wait(return_when=ALL_COMPLETED)并带超时的标题
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # 创建一个线程池,最大工作线程数为3
futures_all = [] # 创建一个空列表用于存储Future对象
futures_all.append(executor.submit(complex_calc, "X", 1.0)) # 提交任务X,耗时1秒
futures_all.append(executor.submit(complex_calc, "Y", 2.0, True)) # 提交任务Y,耗时2秒,模拟失败
futures_all.append(executor.submit(complex_calc, "Z", 4.0)) # 提交任务Z,耗时4秒
print("等待所有任务完成,最长等待 3.5 秒...") # 打印等待所有任务完成,最长等待3.5秒的提示
# 等待所有任务完成,但总等待时间不超过3.5秒
done, not_done = concurrent.futures.wait(futures_all, timeout=3.5, return_when=concurrent.futures.ALL_COMPLETED) # 等待所有Future对象完成,设置超时3.5秒
print(f"\n等待结束。已完成数量: {
len(done)}, 未完成数量: {
len(not_done)}") # 打印等待结束信息、已完成和未完成任务的数量
print("已完成的任务结果:") # 打印已完成任务结果的标题
for f in done: # 遍历已完成的Future对象
try: # 尝试获取结果
print(f" - {
f._fn_args[0]} 结果: {
f.result()}") # 打印任务ID和结果
except Exception as e: # 捕获异常
print(f" - {
f._fn_args[0]} 发生异常: {
e}") # 打印任务ID和发生的异常
print("未完成的任务列表:") # 打印未完成任务列表的标题
for f in not_done: # 遍历未完成的Future对象
print(f" - {
f._fn_args[0]} (状态: running={
f.running()}, done={
f.done()})") # 打印未完成任务的ID和状态
# 强制等待未完成的任务结束,以便Executor可以正常关闭
f.result() # 阻塞等待剩余任务完成
print("所有任务已处理完毕或超时。") # 打印所有任务已处理完毕或超时的提示
print("\n--- 演示 wait(return_when=FIRST_EXCEPTION) ---") # 打印演示wait(return_when=FIRST_EXCEPTION)的标题
with concurrent.futures.ProcessPoolExecutor(max_workers=2) as executor: # 创建一个进程池,最大工作进程数为2
futures_exception = [] # 创建一个空列表用于存储Future对象
futures_exception.append(executor.submit(complex_calc, "FailFast", 3.0, True)) # 提交任务FailFast,耗时3秒,模拟失败
futures_exception.append(executor.submit(complex_calc, "SlowOK", 5.0, False)) # 提交任务SlowOK,耗时5秒,不失败
futures_exception.append(executor.submit(complex_calc, "QuickOK", 1.0, False)) # 提交任务QuickOK,耗时1秒,不失败 (可能先完成但没异常)
print("等待第一个异常或所有任务完成...") # 打印等待第一个异常或所有任务完成的提示
# 等待直到出现第一个异常,或所有任务完成(如果都没异常)
done, not_done = concurrent.futures.wait(futures_exception, return_when=concurrent.futures.FIRST_EXCEPTION) # 等待第一个异常或所有Future对象完成
print(f"\n等待结束。已完成数量: {
len(done)}, 未完成数量: {
len(not_done)}") # 打印等待结束信息、已完成和未完成任务的数量
print("已完成的任务:") # 打印已完成任务的标题
for f in done: # 遍历已完成的Future对象
try: # 尝试获取结果
result = f.result() # 获取Future结果
print(f" - {
f._fn_args[0]} 结果: {
result}") # 打印任务ID和结果
except Exception as e: # 捕获异常
print(f" - {
f._fn_args[0]} 抛出异常: {
e}") # 打印任务ID和抛出的异常
print("未完成的任务:") # 打印未完成任务的标题
for f in not_done: # 遍历未完成的Future对象
print(f" - {
f._fn_args[0]} (状态: running={
f.running()}, done={
f.done()})") # 打印任务ID和状态
# 强制等待未完成的任务结束
f.result() # 阻塞等待剩余任务完成
print("所有任务已处理完毕或异常触发。") # 打印所有任务已处理完毕或异常触发的提示
代码解析:
- 我们通过
concurrent.futures.wait()
函数,演示了三种return_when
参数的使用:FIRST_COMPLETED
:一旦有任何一个任务完成就立即返回。这对于需要处理第一个可用结果的场景非常有用。ALL_COMPLETED
:等待所有任务都完成。这是默认行为,通常在需要确保所有计算都完成后再进行下一步时使用。结合timeout
参数,可以防止无限期等待。FIRST_EXCEPTION
:等待直到第一个任务抛出异常,或者所有任务都成功完成。这在批处理任务中,一旦发现错误就想立即停止并处理异常的场景很有用。
wait()
的返回值是一个元组,包含done
和not_done
两个集合,这使得我们能够清晰地知道哪些任务已经完成,哪些还在进行中。- 注意:
wait()
函数不会获取任务的结果或异常,它只是改变了Future对象的状态并将其分类。你需要遍历done
集合,然后对每个Future
调用result()
或exception()
来实际获取结果或异常。
2.4 Future 对象的高级概念与应用模式
Future
对象不仅仅是并发结果的占位符,它还支持一些高级概念和应用模式,使得在复杂场景下的并发编程更加优雅和健壮。
2.4.1 Future
链式调用与依赖管理
虽然concurrent.futures
本身没有像asyncio
或Tornado
那样提供直接的链式调用语法(例如Promise then()
),但我们可以通过add_done_callback
和再次提交任务到Executor
的方式,间接实现任务的依赖关系和链式调用。
场景:任务B依赖于任务A的结果,任务C依赖于任务B的结果。
import concurrent.futures
import time
def task_A(input_data):
# 任务A:初始处理
print(f"Task A: Starting with {
input_data}...") # 打印任务A开始信息
time.sleep(1) # 模拟处理时间
result_A = f"Processed_{
input_data}_by_A" # 生成任务A的结果
print(f"Task A: Finished, result: {
result_A}") # 打印任务A完成信息
return result_A # 返回结果
def task_B(data_from_A):
# 任务B:依赖于任务A的结果
print(f"Task B: Starting with {
data_from_A}...") # 打印任务B开始信息
time.sleep(1.5) # 模拟处理时间
result_B = f"Transformed_{
data_from_A}_by_B" # 生成任务B的结果
print(f"Task B: Finished, result: {
result_B}") # 打印任务B完成信息
return result_B # 返回结果
def task_C(data_from_B):
# 任务C:依赖于任务B的结果
print(f"Task C: Starting with {
data_from_B}...") # 打印任务C开始信息
time.sleep(0.8) # 模拟处理时间
result_C = f"Finalized_{
data_from_B}_by_C" # 生成任务C的结果
print(f"Task C: Finished, result: {
result_C}") # 打印任务C完成信息
return result_C # 返回结果
def handle_task_A_completion(future_A, executor):
# 处理任务A完成后的回调
try: # 尝试获取任务A的结果
result_A = future_A.result() # 获取任务A的结果
print(f"\nCallback A: Task A completed successfully. Submitting Task B with '{
result_A}'...") # 打印回调A成功信息,并准备提交任务B
# 任务A成功后,提交任务B
future_B = executor.submit(task_B, result_A) # 提交任务B,并传入任务A的结果
future_B.add_done_callback(lambda f_B: handle_task_B_completion(f_B, executor)) # 为任务B注册回调函数,形成链式调用
except Exception as e: # 捕获任务A可能发生的异常
print(f"\nCallback A: Task A failed: {
e}") # 打印任务A失败信息
def handle_task_B_completion(future_B, executor):
# 处理任务B完成后的回调
try: # 尝试获取任务B的结果
result_B = future_B.result() # 获取任务B的结果
print(f"\nCallback B: Task B completed successfully. Submitting Task C with '{
result_B}'...") # 打印回调B成功信息,并准备提交任务C
# 任务B成功后,提交任务C
future_C = executor.submit(task_C, result_B) # 提交任务C,并传入任务B的结果
future_C.add_done_callback(lambda f_C: handle_task_C_completion(f_C)) # 为任务C注册回调函数
except Exception as e: # 捕获任务B可能发生的异常
print(f"\nCallback B: Task B failed: {
e}") # 打印任务B失败信息
def handle_task_C_completion(future_C):
# 处理任务C完成后的回调 (最终回调)
try: # 尝试获取任务C的结果
final_result = future_C.result() # 获取任务C的结果
print(f"\nCallback C: Task C completed successfully. Final result: {
final_result}") # 打印回调C成功信息和最终结果
except Exception as e: # 捕获任务C可能发生的异常
print(f"\nCallback C: Task C failed: {
e}") # 打印任务C失败信息
print("--- 演示 Future 链式调用与依赖管理 ---") # 打印标题
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: # 创建一个线程池,最大工作线程数为3
initial_data = "InitialInput" # 定义初始输入数据
print(f"主程序: 提交 Task A with '{
initial_data}'...") # 打印主程序提交任务A的信息
# 提交第一个任务
future_A = executor.submit(task_A, initial_data) # 提交任务A
# 为任务A注册回调,回调中会提交任务B,任务B的回调中会提交任务C
future_A.add_done_callback(lambda f_A: handle_task_A_completion(f_A, executor)) # 为任务A注册回调函数,并传入executor
print("\n主程序: 所有初始任务已提交,等待链式任务完成...") # 打印主程序等待链式任务完成的提示
# 主程序可能需要等待一段时间,让所有任务链完成
# 在实际应用中,你可能需要一个更复杂的机制来判断所有任务链是否完成
# 例如,收集最终Future并等待它们,或者使用Queue/Event进行通知
time.sleep(6) # 粗略等待所有链式任务完成
print("\n主程序: 演示链式调用结束。") # 打印链式调用结束信息
代码解析:
- 我们定义了三个相互依赖的任务:
task_A
->task_B
->task_C
。 - 关键在于
add_done_callback
:- 当
future_A
完成时,handle_task_A_completion
被调用。它会检查future_A
的结果,如果成功,就使用该结果作为参数提交task_B
,并为task_B
注册handle_task_B_completion
回调。 - 这个过程递归地进行,直到
task_C
完成,其回调handle_task_C_completion
打印最终结果。
- 当
- 这种模式允许我们构建复杂的任务流,其中一个任务的输出作为另一个任务的输入,同时保持异步执行的效率。
- 需要注意:回调函数是在工作者线程/进程中执行的(对于
ThreadPoolExecutor
是在该线程中,对于ProcessPoolExecutor
是在主进程中),所以它们应该尽量轻量级。如果回调本身耗时,考虑将其进一步提交到Executor
中。
2.4.2 Future
与 Queue
的协作:生产者-消费者模式
在并发编程中,生产者-消费者模式是一种常见的范式。Future
对象可以与queue
模块(如queue.Queue
或multiprocessing.Queue
)结合使用,以实现更灵活的数据流管理。
场景:一个或多个生产者任务生成数据,并将数据或包含数据结果的Future
放入队列;一个或多个消费者任务从队列中取出数据或Future
并进行处理。
import concurrent.futures
import time
import random
import queue # 导入队列模块
# 生产者函数:模拟数据生成并提交任务
def data_producer(data_id, output_queue, executor):
print(f"生产者 {
data_id}: 生产数据...") # 打印生产者开始生产数据信息
processing_time = random.uniform(0.5, 2.0) # 随机生成处理时间
time.sleep(processing_time) # 模拟数据生产时间
data_item = f"RawData_{
data_id}" # 生成原始数据项
print(f"生产者 {
data_id}: 提交处理任务 '{
data_item}'...") # 打印生产者提交处理任务信息
future = executor.submit(process_data, data_item) # 提交数据处理任务到Executor
output_queue.put((data_id, future)) # 将数据ID和Future对象放入队列
return f"生产者 {
data_id} 完成" # 返回生产者完成信息
# 数据处理函数:模拟实际的数据处理
def process_data(data):
print(f"处理者: 正在处理 '{
data}'...") # 打印处理者正在处理数据信息
process_time = random.uniform(1.0, 3.0) # 随机生成处理时间
time.sleep(process_time) # 模拟数据处理时间
if random.random() < 0.1: # 10%的概率模拟处理失败
raise ValueError(f"处理 '{
data}' 时发生错误!") # 抛出ValueError
processed_result = f"Cleaned_{
data}_Result" # 生成处理后的结果
print(f"处理者: 完成处理 '{
data}' -> '{
processed_result}'") # 打印处理者完成处理信息
return processed_result # 返回处理结果