想象你在一座城市里同时要寄同城急件和跨城重货。以前你得跑两个快递公司、填两种单子、记两套查询电话;现在“准时达调度中心”开了一个统一柜台:无论交给骑手小队还是运输车队,你都在同一窗口下单、同一张票号取货、同一套规则处理延误和破损。Python 的 concurrent.futures 模块,正是这个统一柜台。它把线程池(ThreadPoolExecutor)和进程池(ProcessPoolExecutor)包装成同一种 Executor 形象,让你用几乎相同的代码提交任务、拿回 Future 凭证、取回最终结果。这样,写并发程序就像寄快递一样简单:不用关心底层是骑手还是卡车,只需把包裹放上柜台。

岗位关联

掌握这套统一并发接口,意味着你写并行数据加载、批量推理、超参搜索时,不再被线程/进程的底层差异绊住,能把精力放在任务本身,这是 AI 工程中必备的“调度基本功”。

目录
  1. 可视化演示
  2. 统一接口理念
  3. 线程池创建使用
  4. 进程池创建使用
  5. Future对象操作
  6. 任务提交方式
  7. 结果获取与超时
  8. 异常处理机制
  9. 原生接口对比
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示线程池任务分发机制、Future对象状态流转(Pending→Running→Done)、以及submit与map两种任务提交模式对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

线程池任务分发 执行器将任务分发给空闲工作线程 忙碌线程执行任务显示进度 空闲线程等待新任务分配

统一接口理念

你是一家 AI 公司的算法工程师,每天要跑数据清洗、模型推理和结果汇总。过去你手里可能有三四套并发代码:multiprocessing.Pool、threading + Queue、手写异步回调。每一套都像不同的快递公司:单号规则不一样,取件窗口不一样,报错方式更是五花八门。现在你决定把所有任务交给同一个调度中心——concurrent.futures。

这个调度中心的核心设计是 Executor:一个抽象柜台。柜台背后有两种运力——ThreadPoolExecutor 和 ProcessPoolExecutor。线程池像同城骑手,进程池像跨城车队;但对你来说,交任务的窗口、拿取件单的流程完全一致。你不需要知道每一次配送是由哪个工人完成的,只要把包裹放在柜台上,拿到单号,等着取件。

在代码层面,模块暴露了统一的 submit、map、shutdown 等方法。你写一次提交逻辑,后续通过选择线程池还是进程池切换执行后端。这个“接口统一”的价值,是让并发代码从“螺蛳壳里做道场”变成“一个模板走天下”。底层实现怎么变,调用方几乎不用改。

有了这个统一柜台,切换后端就像换一家合作快递公司:单号格式、取件流程都不变,变的只是幕后运力。对 AI 从业者来说,这套抽象能让你把时间花在任务本身,而不是反复适应不同并发库的怪脾气。

Executor={ThreadPoolExecutor, ProcessPoolExecutor}\text{Executor} = \{ \text{ThreadPoolExecutor},\ \text{ProcessPoolExecutor} \}
这个公式表示 Executor 是一个抽象概念,实际实现只有线程池和进程池两种。就像“柜台”只是一个概念,柜台背后是骑手队或车队。
  1. Executor(调度柜台)这是两种执行器的共同抽象接口,定义了统一的提交与关闭方法。
    Executor\text{Executor}
  2. 集合符号 = { …… }表示 Executor 的实现集合中只有两个成员,没有第三种官方内置实现。
    { }\{\ \}
  3. ThreadPoolExecutor线程池实现,线程共享同一进程内存,适合 I/O 密集任务。
    ThreadPoolExecutor\text{ThreadPoolExecutor}
  4. ProcessPoolExecutor进程池实现,每个工作进程独立内存,适合 CPU 密集任务。
    ProcessPoolExecutor\text{ProcessPoolExecutor}

线程池创建使用

同城快递骑手小队的特点是:人都在同一个站点里,共享一辆面包车。他们轻装快跑、随时送货上门,特别适合那种“等红灯、等电梯、等用户开门”的小件任务。ThreadPoolExecutor 就是这支骑手小队:线程之间共享进程内存,创建开销小,适合 I/O 密集任务,比如网络请求、文件读写。

使用很简单:with ThreadPoolExecutor(max_workers=4) as pool: 创建线程池,然后通过 pool.submit(func, arg) 提交任务,或者 pool.map(func, iterable) 批量提交。max_workers 是骑手人数,也就是同时能跑多少个线程;上下文管理器退出时会自动调用 shutdown(wait=True),等所有已提交任务完成再关闭站点。

线程池的并发能力不是无限大的。假设有 N 个任务、W 个 worker,每个任务耗时大致相同,那么理论总耗时可以这样估算:

骑手小队适合跑腿,但不适合搬运重型机械。类比到 Python:如果任务全是 CPU 密集计算,线程池会被 GIL 这张“单车窄桥”卡住,因为同一时刻只能有一个线程执行 Python 字节码。这时候就要换用进程池。

T总≈⌈NW⌉×T单任务T_{\text{总}} \approx \left\lceil \frac{N}{W} \right\rceil \times T_{\text{单任务}}
这是最朴素的“排队吞吐”估算:任务数除以工人数,向上取整后乘以单个任务耗时。实际中任务耗时有波动,所以只是估算量级。
  1. N(任务总数)要处理的全部小件包裹数量。
    NN
  2. W(worker 数)同时工作的骑手人数,对应 max_workers。
    WW
  3. 向上取整符号因为不能派半个人,所以商数向上取整。
    ⌈NW⌉\left\lceil \frac{N}{W} \right\rceil
  4. 除以总耗时每个 worker 串行处理自己分到的任务,总时间就是“轮次数”乘以单任务时间。
    ×T单任务\times T_{\text{单任务}}

进程池创建使用

跨城运输车队和同城骑手完全不同:每辆卡车有独立的仓库、独立的路线、独立的小团队。它们不共享货物箱,搬运重货时互不干扰。ProcessPoolExecutor 就是这样一支车队:每个工作进程拥有独立内存空间,可以真正并行利用多核 CPU,适合 CPU 密集任务,比如特征工程、数值计算、模型推理。

创建方式与线程池几乎一致:with ProcessPoolExecutor(max_workers=4) as pool:。因为进程不共享内存,任务函数和参数必须能被 pickle 序列化;在 Windows 上,还必须把创建进程池的代码放在 if __name__ == "__main__": 保护下,否则会递归创建子进程。

为什么换成进程池就能跑得快?因为每个进程都有自己的 GIL,相当于每座仓库都有独立的进港大桥,多辆卡车可以同时装卸重货。理想情况下,并行耗时与串行耗时的关系是:

进程池最大的成本是“建队”:每创建一个进程都要重新搬仓库、初始化解释器。所以别拿它处理几十毫秒的小任务,否则建车队的功夫比跑腿还久。任务够重、计算够密集,才值得动用车队。

S=T串行T并行S = \frac{T_{\text{串行}}}{T_{\text{并行}}}
S 叫加速比。如果任务能完美并行,S 会接近 CPU 核数;现实中有通信和调度开销,S 往往小于核数。
  1. T_串行用一个进程逐个完成任务的总耗时。
    T串行T_{\text{串行}}
  2. T_并行多个进程同时开工后的总耗时。
    T并行T_{\text{并行}}
  3. S(加速比)串行耗时除以并行耗时,S 越大,并行收益越高。
    S=T串行T并行S = \frac{T_{\text{串行}}}{T_{\text{并行}}}
  4. 理想上限如果 W 个进程均分任务,S 最多接近 W。
    S≈WS \approx W

Future对象操作

你去快递柜台寄件时,工作人员会给你一张取件单号。这张单号不代表包裹已经送到,只代表“你的包裹已经录入系统”。Future 就是这张取件单号:submit 提交任务后立刻返回一个 Future 对象,任务可能还在运行,但你已经拥有了查询和取结果的权利。

Future 的核心操作很直白:done() 判断任务是否完成,不阻塞;result() 阻塞等待任务完成并返回结果;cancel() 尝试取消还没开始执行的任务。如果任务已经跑起来了,cancel() 会返回 False。这就像看到物流信息“配送中”时,你想取消也来不及了。

一个 Future 内部维护着任务状态,可以用集合语言表示:

所以 Future 并不神秘,它就是“结果暂存器”。你把任务交给执行器,执行器立刻还你一张单号;等任务跑完,结果会被装进单号对应的包裹里,等你来取。

Future 状态∈{等待/运行, 完成, 已取消, 异常}\text{Future 状态} \in \{ \text{等待/运行},\ \text{完成},\ \text{已取消},\ \text{异常} \}
每个 Future 都处于四种状态之一。调用 result() 时,如果状态是“完成”,就返回结果;如果是“异常”,就抛出异常;如果是“已取消”,同样会报错。
  1. Future 状态取件单号背后记录的物流状态,不是任务本身。
    Future 状态\text{Future 状态}
  2. 等待/运行任务已经提交,但还没给最终结果。
    等待/运行\text{等待/运行}
  3. 完成任务成功结束,结果已封装在 Future 中。
    完成\text{完成}
  4. 已取消/异常任务被取消,或执行过程中出了问题。
    已取消/异常\text{已取消/异常}

任务提交方式

快递柜台有两种下单方式:寄一件,就手写一张面单;寄一百件,就用批量打单机唰唰唰打出一摞面单。submit 是前者,map 是后者。submit(func, *args) 一次提交一个任务,返回一个 Future;map(func, *iterables) 一次提交整个可迭代对象,返回一个按输入顺序产出结果的迭代器。

如果你需要对每个 Future 单独做超时控制、取消操作或异常判断,用 submit 更灵活;如果你只是想把一个函数套到一批数据上,按顺序拿结果,map 更省事。map 还有一个细节:它返回的迭代器在迭代时才会阻塞等待对应结果,所以可以在拿到部分结果后提前退出。

用数学语言描述 map 的语义就是:

submit 和 map 不是竞争关系,而是互补工具。需要“一单一查”时用 submit,需要“整批处理”时用 map。好的并发代码,往往是在这两者之间做清晰取舍。

map(f, [x1,⋯ ,xn])⇒[f(x1),⋯ ,f(xn)]\text{map}(f,\ [x_1,\cdots,x_n]) \Rightarrow [f(x_1),\cdots,f(x_n)]
这表示对列表里的每个元素依次应用函数 f,最终按原顺序得到一组结果。顺序不变,是 map 最直观的契约。
  1. map批量下单接口,对应快递柜台的批量打单机。
    map\text{map}
  2. 函数 f要应用到每个元素上的处理函数。
    ff
  3. 输入列表待处理的原始数据序列。
    [x1,⋯ ,xn][x_1,\cdots,x_n]
  4. 结果列表每个输入元素经过 f 后得到的结果,顺序一一对应。
    [f(x1),⋯ ,f(xn)][f(x_1),\cdots,f(x_n)]

结果获取与超时

你拿着取件单号去窗口取件,工作人员说:“这单会在 5 分钟内送到,超时的话窗口会先报个提醒,但包裹到了还是能取。”result(timeout) 就是这种限时取件逻辑。result(timeout=5) 表示最多等 5 秒;如果 5 秒后任务没完成,就抛出 TimeoutError,但任务本身不会停,你之后再次调用 result() 仍能拿到结果。

批量任务也有对应的限时策略:as_completed(fs, timeout=3) 会监听一组 Future,谁先完成就先返回谁,而不是按提交顺序排队。整体等待超过 timeout 时,同样抛出 TimeoutError。这就像把所有单号一起交给窗口:“哪个包裹先到,就先叫我取哪个。”

超时判断在时间轴上可以写成:

超时是并发场景里的“安全绳”。AI 任务经常要跑多路请求,如果某个数据源卡住 10 分钟,整条流水线都会跟着发呆。用 result(timeout) 或 as_completed(timeout) 给每个任务设好上限,就等于给流水线装了保险阀。

等待时间>timeout⇒TimeoutError\text{等待时间} > \text{timeout} \Rightarrow \text{TimeoutError}
公式左边是真实等待时长,右边是用户设定的时间上限;一旦左边超过右边,调用方就会收到 TimeoutError,而不是永远等下去。
  1. 等待时间从调用 result() 那一刻起,到当前为止的实际等待时长。
    等待时间\text{等待时间}
  2. timeout你愿意等待的最大秒数,可以是小数。
    timeout\text{timeout}
  3. 比较符号判断是否已经超时。
    >>
  4. TimeoutError超时后抛出的异常类型,不表示任务失败,只表示“你等不起了”。
    TimeoutError\text{TimeoutError}

异常处理机制

快递运输途中,包裹可能破损、退回。快递公司不会因为某个包裹出问题就炸掉整个站点,而是会给这个包裹打上“异常”标记。Future 对待异常也是这样:worker 函数里抛出的异常不会被立即传播到主线程,而是被捕获并“封箱”保存到 Future 内部。

当你在主线程调用 future.result() 时,就像拆开这个异常包裹,里面的 traceback 会被重新抛出。所以你会发现:任务是在线程池/进程池里报的错,但异常出现在你调用 result() 的那一行。如果想要“只检查不拆箱”,可以用 future.exception(timeout),返回异常对象本身而不是抛出它。

异常在 Future 中的流转可以表示为:

这个机制的好处是:一个任务失败不会拖垮其他任务。你可以在 as_completed 循环里单独处理每个 Future 的异常,就像快递网点收到异常包裹时,只会单独记录,不会停止整个配送网络。

worker 异常→Future 捕获保存异常→result()调用方抛出\text{worker 异常} \xrightarrow{\text{Future 捕获}} \text{保存异常} \xrightarrow{\text{result()}} \text{调用方抛出}
异常先在 worker 里产生,然后被 Future 捕获并保存,最后当你调用 result() 时,异常从调用方位置重新抛出。整个过程是“延迟交付”。
  1. worker 异常任务函数内部抛出的异常,比如除零、网络超时。
    worker 异常\text{worker 异常}
  2. Future 捕获执行器拦截异常,不让它直接崩溃整个进程。
    →Future 捕获\xrightarrow{\text{Future 捕获}}
  3. 保存异常异常对象被存进 Future,等待调用方查看。
    保存异常\text{保存异常}
  4. 调用方抛出调用 result() 时异常被重新激活,主线程按正常 try/except 处理。
    →result()调用方抛出\xrightarrow{\text{result()}} \text{调用方抛出}

原生接口对比

在 concurrent.futures 出现之前,Python 并行代码是“各快递公司各搞一套单号系统”。multiprocessing.Pool 有 apply、apply_async、map、map_async、starmap_async……方法很多,但返回的 AsyncResult 对象和 Future 接口并不完全一样,命名也不统一。换个场景就要重学一套用法。

concurrent.futures 把这些杂牌单号统一成 Future。无论你用的是线程池还是进程池,拿到的都是同一个 Future 对象,调用 result()、done()、exception() 的方式完全一样。原生 Pool 也能用,但它的异步接口更像是“没有标准柜台之前的老手工窗口”。

两种方式的核心对应关系可以概括为:

原生 Pool 并不是不能用了,只是它的接口更零散,需要手动拼装。迁移到 concurrent.futures 后,代码更统一、更容易在“线程池/进程池”之间切换。这就是标准化的力量:新系统不一定每件事都做得更多,但让日常操作变得简单可靠。

Pool.apply_async(f, args)≈Executor.submit(f, ∗args)\text{Pool.apply\_async}(f,\ \text{args}) \approx \text{Executor.submit}(f,\ *\text{args})
老 Multiprocessing Pool 的 apply_async 和新 Executor 的 submit 在功能上很接近:都是异步提交单个任务并拿到一个结果对象。但新接口更统一,Future 的后续操作也更丰富。
  1. Pool.apply_async老派进程池的异步提交方法,返回 AsyncResult 对象。
    Pool.apply_async\text{Pool.apply\_async}
  2. 函数参数 f要执行的任务函数。
    ff
  3. args以元组形式传入的位置参数。
    args\text{args}
  4. Executor.submit新统一接口的异步提交方法,返回 Future 对象,接口更整洁。
    ≈Executor.submit(f, ∗args)\approx \text{Executor.submit}(f,\ *\text{args})

AI 应用场景

数据管道并行下载与预处理

阶段:数据准备与特征工程

解决的问题:训练前要从多个数据源下载图片、读取 JSON、做缩放清洗。传统串行下载耗时以小时计,手写线程队列又容易死锁。使用 ThreadPoolExecutor,把每个文件当作一个“包裹”提交给统一柜台,骑手们同时外出取件,代码量比手写 threading 少一半。

具体案例:某图像分类项目需要下载 5 万张图片并做缩略图。with ThreadPoolExecutor(max_workers=16) as pool: results = list(pool.map(download_and_resize, urls)),串行 3 小时的流程压缩到 20 分钟。

CPU 密集的批量特征提取与超参搜索

阶段:模型训练与调优

解决的问题:特征提取、距离计算、小规模模型推理是 CPU 密集任务,用线程池会被 GIL 卡住;改用 ProcessPoolExecutor 后,多个进程同时占用多核,再把每个任务的结果封装进 Future,统一用 as_completed 收集,天然支持超时和异常隔离。

具体案例:AutoML 工具要对 100 组超参分别训练轻量模型。ProcessPoolExecutor(max_workers=8) 并行执行 8 个训练任务,每组超参的验证分数通过 future.result(timeout=600) 获取,超时的实验单独记录,不会拖垮整体搜索。

Python 演示

python39 行
import concurrent.futuresimport mathimport time def is_prime(n):    if n < 2:        return False    for i in range(2, int(math.sqrt(n)) + 1):        if n % i == 0:            return False    return True def check_prime(n):    return n, is_prime(n) def main():    numbers = list(range(10**6 + 10000, 10**6 + 10050))     # 线程池执行同一批任务    with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:        start = time.time()        list(pool.map(check_prime, numbers))        elapsed_thread = time.time() - start        print("线程池耗时:", round(elapsed_thread, 3), "秒")     # 进程池执行同一批任务    with concurrent.futures.ProcessPoolExecutor(max_workers=4) as pool:        start = time.time()        futures = [pool.submit(check_prime, n) for n in numbers]        prime_count = 0        for future in concurrent.futures.as_completed(futures, timeout=10):            _, is_p = future.result()            if is_p:                prime_count += 1        elapsed_process = time.time() - start        print("进程池耗时:", round(elapsed_process, 3), "秒, 质数个数:", prime_count) if __name__ == "__main__":    main()
目的:用同一批质数检测任务,对比线程池与进程池在 CPU 密集场景下的耗时差异,同时演示 map、submit、as_completed 和 result() 的配合使用。
  1. with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:创建线程池,with 退出时自动等待任务完成。
  2. pool.map(check_prime, numbers)批量提交任务,返回结果按输入顺序排列。
  3. ProcessPoolExecutor(max_workers=4)创建进程池,每个进程独立内存,避开 GIL。
  4. pool.submit(check_prime, n)单个提交任务,立即拿到一个 Future 对象。
  5. as_completed(futures, timeout=10)谁先完成就先迭代谁,不再按提交顺序等待。
  6. future.result()阻塞获取结果;若任务抛出异常,异常会在这个位置重新抛出。

常见误区

误区我用了 `ThreadPoolExecutor`,为什么 CPU 密集型程序反而变慢了?是不是 `max_workers` 开得不够大?我应该继续加大线程数。

正解在 CPython 中,GIL 保证同一时刻只有一个线程执行 Python 字节码,所以纯计算的多个线程会轮流抢锁,几乎无法并行。线程池适合 I/O 密集任务,因为网络请求和文件读写在等待时会让出 GIL。CPU 密集任务应该改用 ProcessPoolExecutor,用独立进程绕过 GIL。

为何常见:新手容易把“线程”等同于“并行计算”,忽略了 GIL 的存在;再加上线程创建开销小,容易盲目加线程。

误区我提交完任务后主程序马上执行完了,为什么进程池里有些任务没跑完?是不是我的代码写错了?

正解submit 只是把任务排进队列,主线程如果直接退出,进程池可能没来得及处理完所有任务。正确做法是用 with 语句,或手动调用 shutdown(wait=True),让执行器等所有已提交任务完成后再退出。

为何常见:把“提交任务”和“任务完成”混为一谈。提交只代表快递已揽收,不代表包裹已签收。

误区`future.result(timeout=3)` 超时抛了 `TimeoutError`,我以为这个任务已经被终止了,结果资源还在被占用。

正解timeout 只是调用方愿意等待的时长限制。超时后任务仍在后台继续运行,Future 也仍然有效;之后再次调用 result() 还是能拿到结果。真正要取消任务需要用 future.cancel(),但已经启动的任务也取消不了。

为何常见:日常生活中的“超时”往往意味着订单作废,但并发任务里的超时只是“我不等了”,不是“你别干了”。

误区我把一个 lambda 函数传给 `ProcessPoolExecutor.submit`,结果报错 `PicklingError`。lambda 在普通代码里都能用,为什么进程池里就不行?

正解进程池的 worker 是新进程,任务函数和参数必须通过 pickle 序列化后传递。lambda 不是模块级函数,无法被 pickle;普通类实例、文件对象、锁对象也可能无法序列化。线程池共享内存,没有这个限制。

为何常见:线程池和进程池 API 几乎一样,让人觉得两者对参数的约束也一样,忽略了进程独立内存带来的序列化成本。

面试题

Q1. ThreadPoolExecutor 和 ProcessPoolExecutor 的共同点是什么?

单选题 · easy

  • A.都使用统一的任务提交接口← 正确答案
  • B.都使用独立进程来执行任务
  • C.都无需任何参数即可创建
  • D.都一次只能提交一个任务

答案:A。两者都继承自 Executor 抽象,都支持 submit、map、shutdown 等统一 API。B 只有进程池满足,C 不准确,D 明显错误。就像同城骑手和跨城车队虽然运力不同,但都在同一个柜台下单。

Q2. 要创建一个最多 4 个线程同时工作的线程池,正确的是?

单选题 · easy

  • A.ThreadPoolExecutor(max_workers=4)← 正确答案
  • B.ProcessPoolExecutor(max_workers=4)
  • C.Executor(max_workers=4)
  • D.Pool(max_workers=4)

答案:A。创建线程池必须用 ThreadPoolExecutor,max_workers=4 表示最多 4 个线程。B 创建的是进程池,C 中 Executor 是抽象类不能直接实例化,D 的 Pool 是 multiprocessing 老接口。

Q3. 提交任务后返回的 Future 对象,哪个方法会阻塞等待任务完成并返回结果?

单选题 · mid

  • A.get() 直接获取结果
  • B.result() 阻塞取回结果← 正确答案
  • C.wait() 等待任务完成
  • D.done() 查询是否结束

答案:B。Future.result() 会阻塞直到任务完成,然后返回结果;如果任务异常,还会重新抛出异常。get() 是老异步库的方法,done() 不阻塞,wait() 不是 Future 的方法。

Q4. 关于 future.result(timeout=3),下列说法正确的是?

单选题 · mid

  • A.超过 3 秒则立即终止任务
  • B.超过 3 秒抛出超时但任务继续← 正确答案
  • C.超过 3 秒自动返回空值 None
  • D.超过 3 秒进程池会直接崩溃

答案:B。timeout 只限制调用方的等待时间,超时抛 TimeoutError,但任务本身继续运行,之后还能再次调用 result() 取结果。A、C、D 都误解了“超时取件”只是窗口不等人,不是包裹被销毁。

Q5. 以下哪种任务更适合使用 ThreadPoolExecutor?

单选题 · mid

  • A.大量 CPU 密集型数值计算
  • B.网络请求和文件读写等待← 正确答案
  • C.大规模分布式模型同步训练
  • D.纯 Python 循环整数累加

答案:B。线程池适合 I/O 密集任务,因为在等待网络或磁盘时会释放 GIL,让其他线程跑起来。A、D 是 CPU 密集任务,受 GIL 限制;C 的规模远超单机线程池能解决的问题。

Q6. 在 ProcessPoolExecutor 中提交的任务函数和参数,必须满足什么条件?

单选题 · hard

  • A.必须可以被 pickle 序列化← 正确答案
  • B.只能是整数或字符串等基础类型
  • C.必须定义在全局作用域中
  • D.必须通过线程锁保护后传入

答案:A。进程池通过进程间通信传数据,函数和参数要经过 pickle 序列化。lambda 或局部函数通常无法 pickle。B 太绝对,自定义类也可能可序列化;C 不强制,D 与线程锁无关。

Q7. 关于 as_completed(fs) 的描述,正确的是?

单选题 · hard

  • A.按任务提交顺序依次返回结果
  • B.按任务完成顺序逐个返回 Future← 正确答案
  • C.必须传入任务结果而不能传 Future
  • D.自动吞掉 worker 的异常并继续

答案:B。as_completed 监听多个 Future,谁先完成就先把谁 yield 出去,而不是按提交顺序排队。它接收的是 Future 列表,异常仍需调用 result() 时处理,不会自动吞掉。

Q8. 使用 ProcessPoolExecutor 时,以下哪些说法正确?

多选题 · mid

  • A.每个 worker 是一个独立进程← 正确答案
  • B.可以更好地利用多核 CPU← 正确答案
  • C.适合 CPU 密集型任务← 正确答案
  • D.参数依赖 pickle,lambda 可能报错← 正确答案

答案:ABCD。进程池的每个 worker 都是独立进程,各自拥有独立的 GIL 和内存空间,因此能并行利用多核,适合 CPU 密集任务。由于进程间需要序列化,lambda 这类无法 pickle 的对象会报错。所以四项都正确。

Q9. 关于 Future 的异常处理,以下哪些说法正确?

多选题 · hard

  • A.任务抛出的异常会保存在 Future 内部← 正确答案
  • B.调用 result() 时会重新抛出该异常← 正确答案
  • C.exception() 方法可获取异常对象← 正确答案
  • D.只有进程池任务才会产生异常

答案:ABC。worker 异常会被捕获并存入 Future,调用 result() 或 exception() 时才会暴露出来。线程池和进程池的任务都可能异常,D 过于绝对,所以不选。

Q10. 在 Windows 上使用 ProcessPoolExecutor,哪项做法是必须的?

单选题 · extreme

  • A.进程池入口代码放在 main 保护下← 正确答案
  • B.线程池必须关闭自动重连机制
  • C.进程池无法使用 with 语句管理
  • D.只能在交互式环境中创建进程池

答案:A。Windows 没有 fork,创建子进程时会重新导入主模块,若不放在 if __name__ == "__main__": 保护下,可能无限递归创建进程。B、C、D 都是错误说法。这属于平台相关的“极端注意事项”。