【Python】concurrent

第一章:并发编程的基石与 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 语法 (Python asyncio):通过语言特性支持的协程实现非阻塞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:

  1. I/O操作:当线程执行文件读写、网络请求等I/O操作时,它会主动释放GIL,允许其他线程运行。这是因为I/O操作通常涉及到等待外部资源,这段等待时间CPU是空闲的,此时释放GIL可以提高整体效率。
  2. 达到时间片:CPython解释器内部会有一个机制,当一个线程持有GIL并执行了一段时间(通常是100个字节码指令或15毫秒,具体取决于版本和实现)后,它会强制释放GIL,即使它还没有完成当前任务,以便其他线程有机会运行。
  3. 主动释放:一些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的并发编程主要依赖于threadingmultiprocessing模块。这两个模块提供了创建和管理线程/进程的基本API,但它们在使用上存在一些痛点:

1.3.1 传统多线程/多进程编程的痛点
  1. 手动管理线程/进程生命周期:开发者需要手动创建、启动、管理、等待线程/进程的完成,并在完成后进行清理。这增加了代码的复杂性和出错的风险。
  2. 结果收集困难:从子线程或子进程获取函数的返回值并不直接。通常需要通过Queue、共享变量(需要锁保护)或管道等机制来回传结果,这使得代码结构变得复杂。
  3. 异常处理复杂:子线程/子进程中发生的未捕获异常默认不会传播到主线程/主进程,开发者需要额外的机制(如try-except块结合Queue)来捕获并处理这些异常。
  4. 资源限制和开销:频繁地创建和销毁线程/进程会带来显著的开销。操作系统对可创建的线程/进程数量也有上限。
  5. 池化管理缺失:没有内置的线程池或进程池概念。开发者需要自己实现或使用第三方库来复用线程/进程,以减少创建和销毁的开销。
1.3.2 concurrent.futures 的诞生与核心理念

为了解决上述痛点,Python 3.2引入了concurrent.futures库。它的核心理念是:将任务提交与结果获取解耦,并提供统一的高级接口来管理线程池和进程池

concurrent.futures库提供了一个高级抽象层,它定义了Executor抽象基类,并提供了两种具体的实现:

  • ThreadPoolExecutor:用于管理线程池,适用于I/O密集型任务。
  • ProcessPoolExecutor:用于管理进程池,适用于CPU密集型任务,能够绕开GIL实现真正的并行。

它通过引入Future对象,极大地简化了异步结果的获取和异常处理。

concurrent.futures在Python并发生态系统中的定位

  • 高级抽象:它是对threadingmultiprocessing模块的封装和抽象,提供了更简洁、更易用的API。
  • 统一接口:无论底层是线程还是进程,concurrent.futures都提供了一致的ExecutorFuture接口,降低了学习成本和代码迁移的难度。
  • 简化任务管理:通过线程池和进程池,它自动化了线程/进程的生命周期管理,包括创建、复用和销毁。
  • 异步结果管理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对象的核心状态通常包括:

  1. PENDING (挂起):任务已提交到Executor,但尚未开始执行。它可能正在等待线程池或进程池中的可用工作者。
  2. RUNNING (运行中):任务已被一个工作者(线程或进程)选中,正在执行中。
  3. CANCELLED (已取消):任务在执行前被明确取消。如果任务已经开始运行,则无法被取消。
  4. FINISHED (已完成):任务已经完成执行。完成状态又分为两种子状态:
    • SUCCEEDED (成功):任务执行成功,并返回了一个结果。
    • FAILED (失败):任务执行过程中抛出了一个未捕获的异常。

这些状态之间的转换是单向的:

  • PENDING -> RUNNING (任务开始执行)
  • PENDING -> CANCELLED (任务在执行前被取消)
  • RUNNING -> FINISHED (任务执行完毕,无论是成功还是失败)
  • CANCELLEDFINISHED 都是最终状态,一旦进入这些状态,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所代表的任务是否已经完成。如果任务已完成(即状态为CANCELLEDFINISHED),则返回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
  • 这个例子很好地展示了任务从PENDINGRUNNING再到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或高级并发结构时可能会用到,但在日常使用ThreadPoolExecutorProcessPoolExecutor时,它们是透明的。

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_completedtimeout参数。它指定了在两次连续的Future产出之间等待的最长时间。如果在此时间内没有新的Future完成,就会抛出TimeoutError。这与future.result()timeout是不同的,future.result()是等待单个Future完成,而as_completedtimeout是等待下一个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()的返回值是一个元组,包含donenot_done两个集合,这使得我们能够清晰地知道哪些任务已经完成,哪些还在进行中。
  • 注意wait()函数不会获取任务的结果或异常,它只是改变了Future对象的状态并将其分类。你需要遍历done集合,然后对每个Future调用result()exception()来实际获取结果或异常。
2.4 Future 对象的高级概念与应用模式

Future对象不仅仅是并发结果的占位符,它还支持一些高级概念和应用模式,使得在复杂场景下的并发编程更加优雅和健壮。

2.4.1 Future链式调用与依赖管理

虽然concurrent.futures本身没有像asyncioTornado那样提供直接的链式调用语法(例如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 FutureQueue 的协作:生产者-消费者模式

在并发编程中,生产者-消费者模式是一种常见的范式。Future对象可以与queue模块(如queue.Queuemultiprocessing.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 # 返回处理结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值