理解Python并发编程-PoolExecutor篇

本文介绍Python的concurrent.futures模块,该模块提供ThreadPoolExecutor和ProcessPoolExecutor类,简化多线程和多进程编程。通过示例展示如何使用这些类来执行异步任务,包括异常处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前我们使用多线程(threading)和多进程(multiprocessing)完成常规的需求,在启动的时候start、jon等步骤不能省,复杂的需要还要用1-2个队列。随着需求越来越复杂,如果没有良好的设计和抽象这部分的功能层次,代码量越多调试的难度就越大。有没有什么好的方法把这些步骤抽象一下呢,让我们不关注这些细节,轻装上阵呢?

答案是:有的

从Python3.2开始一个叫做concurrent.futures被纳入了标准库,而在Python2它属于第三方的futures库,需要手动安装:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
     
❯ pip install futures
```
这个模块中有 2个类:ThreadPoolExecutor和ProcessPoolExecutor,也就是对threading和multiprocessing的进行了高级别的抽象,
暴露出统一的接口,帮助开发者非常方便的实现异步调用:
```python
import time
from concurrent.futures import ProcessPoolExecutor, as_completed
NUMBERS = range( 25, 38)
def fib(n):
if n<= 2:
return 1
return fib(n -1) + fib(n -2)
start = time.time()
with ProcessPoolExecutor(max_workers= 3) as executor:
for num, result in zip(NUMBERS, executor.map(fib, NUMBERS)):
print 'fib({}) = {}'.format(num, result)
print 'COST: {}'.format(time.time() - start)

感受下是不是很轻便呢?看一下花费的时间:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     
❯ python fib_executor.py
fib(25) = 75025
fib(26) = 121393
fib(27) = 196418
fib(28) = 317811
fib(29) = 514229
fib(30) = 832040
fib(31) = 1346269
fib(32) = 2178309
fib(33) = 3524578
fib(34) = 5702887
fib(35) = 9227465
fib(36) = 14930352
fib(37) = 24157817
COST: 10.8920350075

除了用map,另外一个常用的方法是submit。如果你要提交的任务的函数是一样的,就可以简化成map。但是假如提交的任务函数是不一样的,或者执行的过程之可能出现异常(使用map执行过程中发现问题会直接抛出错误)就要用到submit:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
     
from concurrent.futures import ThreadPoolExecutor, as_completed
NUMBERS = range( 30, 35)
def fib(n):
if n == 34:
raise Exception( "Don't do this")
if n<= 2:
return 1
return fib(n -1) + fib(n -2)
with ThreadPoolExecutor(max_workers= 3) as executor:
future_to_num = {executor.submit(fib, num): num for num in NUMBERS}
for future in as_completed(future_to_num):
num = future_to_num[future]
try:
result = future.result()
except Exception as e:
print 'raise an exception: {}'.format(e)
else:
print 'fib({}) = {}'.format(num, result)
with ThreadPoolExecutor(max_workers= 3) as executor:
for num, result in zip(NUMBERS, executor.map(fib, NUMBERS)):
print 'fib({}) = {}'.format(num, result)

执一下:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
     
❯ python fib_executor_with_raise.py
fib(30) = 832040
fib(31) = 1346269
raise an exception: Don 't do this
fib(32) = 2178309
fib(33) = 3524578
Traceback (most recent call last):
File "fib_executor_with_raise.py", line 28, in <module>
for num, result in zip(NUMBERS, executor.map(fib, NUMBERS)):
File "/Library/Python/2.7/site-packages/concurrent/futures/_base.py", line 580, in map
yield future.result()
File "/Library/Python/2.7/site-packages/concurrent/futures/_base.py", line 400, in result
return self.__get_result()
File "/Library/Python/2.7/site-packages/concurrent/futures/_base.py", line 359, in __get_result
reraise(self._exception, self._traceback)
File "/Library/Python/2.7/site-packages/concurrent/futures/_compat.py", line 107, in reraise
exec('raise exc_type, exc_value, traceback ', {}, locals_)
File "/Library/Python/2.7/site-packages/concurrent/futures/thread.py", line 61, in run
result = self.fn(*self.args, **self.kwargs)
File "fib_executor_with_raise.py", line 9, in fib
raise Exception("Don't do this ")
Exception: Don't do this

可以看到,第一次捕捉到了异常,但是第二次执行的时候错误直接抛出来了。

上面说到的map,有些同学马上会说,这不是进程(线程)池的效果吗?看起来确实是的:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
     
import time
from multiprocessing.pool import Pool
NUMBERS = range(25, 38)
def fib(n):
if n<= 2:
return 1
return fib(n-1) + fib(n-2)
start = time.time()
pool = Pool(3)
results = pool.map(fib, NUMBERS)
for num, result in zip(NUMBERS, pool.map(fib, NUMBERS)):
print 'fib({}) = {}'.format(num, result)
print 'COST: {}'.format(time.time() - start)

好像代码量更小哟。好吧,看一下花费的时间:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
     
❯ python fib_pool.py
fib(25) = 75025
fib(26) = 121393
fib(27) = 196418
fib(28) = 317811
fib(29) = 514229
fib(30) = 832040
fib(31) = 1346269
fib(32) = 2178309
fib(33) = 3524578
fib(34) = 5702887
fib(35) = 9227465
fib(36) = 14930352
fib(37) = 24157817
COST: 17.1342718601

WhatTF竟然花费了1.7倍的时间。为什么?

BTW,有兴趣的同学可以对比下ThreadPool和ThreadPoolExecutor,由于GIL的缘故,对比的差距一定会更多。

原理

我们就拿ProcessPoolExecutor介绍下它的原理,引用官方代码注释中的流程图:

     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     
|======================= In-process =====================|== Out-of-process ==|
+----------+ +----------+ +--------+ +-----------+ +---------+
| | => | Work Ids | => | | => | Call Q | => | |
| | +----------+ | | +-----------+ | |
| | | ... | | | | ... | | |
| | | 6 | | | | 5, call() | | |
| | | 7 | | | | ... | | |
| Process | | ... | | Local | +-----------+ | Process |
| Pool | +----------+ | Worker | | #1..n |
| Executor | | Thread | | |
| | +----------- + | | +-----------+ | |
| | <=> | Work Items | <=> | | <= | Result Q | <= | |
| | +------------+ | | +-----------+ | |
| | | 6: call() | | | | ... | | |
| | | future | | | | 4, result | | |
| | | ... | | | | 3, except | | |
+----------+ +------------+ +--------+ +-----------+ +---------+

我们结合源码和上面的数据流分析一下:

  1. executor.map会创建多个_WorkItem对象,每个对象都传入了新创建的一个Future对象。
  2. 把每个_WorkItem对象然后放进一个叫做「Work Items」的dict中,键是不同的「Work Ids」。
  3. 创建一个管理「Work Ids」队列的线程「Local worker thread」,它能做2件事:
    1. 从「Work Ids」队列中获取Work Id, 通过「Work Items」找到对应的_WorkItem。如果这个Item被取消了,就从「Work Items」里面把它删掉,否则重新打包成一个_CallItem放入「Call Q」这个队列。executor的那些进程会从队列中取_CallItem执行,并把结果封装成_ResultItems放入「Result Q」队列中。
    2. 从「Result Q」队列中获取_ResultItems,然后从「Work Items」更新对应的Future对象并删掉入口。

看起来就是一个「生产者/消费者」模型罢了,错了。我们要注意,整个过程并不是多个进程与任务+结果-2个队列直接通信的,而是通过一个中间的「Local worker thread」,它就是让效率提升的重要原因之一!!!

设想,当某一段程序提交了一个请求,期望得到一个答复。但服务程序对这个请求可能很慢,在传统的单线程环境下,调用函数是同步的,也就是说它必须等到服务程序返回结果后,才能进行其他处理。而在Future模式下,调用方式改为异步,而原先等待返回的时间段,在主调用函数中,则可用于处理其他事物。

Future

Future是常见的一种并发设计模式,在多个其他语言中都可以见到这种解决方案。

一个Future对象代表了一些尚未就绪(完成)的结果,在「将来」的某个时间就绪了之后就可以获取到这个结果。比如上面的例子,我们期望并发的执行一些参数不同的fib函数,获取全部的结果。传统模式就是在等待queue.get返回结果,这个是同步模式,而在Future模式下,调用方式改为异步,而原先等待返回的时间段,由于「Local worker thread」的存在,这个时候可以完成其他工作

在tornado中也有对应的实现。2013年的时候,我曾经写过一篇博客使用tornado让你的请求异步非阻塞,最后也提到了用concurrent.futures实现异步非阻塞的完成耗时任务。

原文链接:http://www.dongwm.com/archives/使用Python进行并发编程-PoolExecutor篇/

<think>好的,用户想了解Python中的并发处理方法,包括多线程、异步IO、asyncio、multiprocessing这些方面。首先,我需要回忆一下Python并发编程的不同方法,以及它们的适用场景优缺点。根据提供的引用资料,用户已经引用了几个相关的知识点,比如多线程适用于I/O密集型任务,但因为GIL的存在,多线程在CPU密集型任务上效果不佳,这时候应该用多进程。而异步IO协程则适合高并发的网络请求,比如爬虫或者Web服务。 接下来,我需要整理这些信息,确保回答结构清晰,分点说明每种方法的原理、适用场景、优缺点,并且结合引用中的例子。比如,引用[4]详细说明了多线程、多进程异步IO的区别,可以引用这部分内容。同时,用户提供的示例中提到了代码示例,所以在回答中也需要加入相应的代码片段,但要注意使用正确的格式,比如Python代码用三个反引号包裹,数学公式用$符号。 然后,要检查是否有遗漏的重要点,比如全局解释器锁(GIL)对多线程的影响,多进程如何绕过GIL,协程的工作原理,以及如何选择合适的方法。需要确保每个部分都有对应的引用标注,比如引用[4]提到GIL的问题,引用[2][3]提到多线程多进程的代码实现。 此外,用户要求生成相关问题,我需要根据内容提出几个相关问题,比如如何选择并发方法、多进程的优缺点、协程的使用场景等,确保问题覆盖用户可能感兴趣的方面。 最后,要确保回答符合用户要求的格式,使用中文,数学公式正确,引用标识自然添加。可能需要调整段落结构,让信息更易读,比如使用标题分隔不同方法,每个部分用项目符号列出关键点,代码示例放在对应的位置。 现在,我需要将这些思考整合成一个结构化的回答,确保每个部分都有逻辑性,引用正确,并且涵盖用户想了解的所有内容。同时,注意避免技术术语过多,保持解释的清晰易懂,适合不同层次的理解者。</think>### Python并发编程方法详解 Python提供了三种主要并发编程方法:**多线程(Threading)**、**多进程(Multiprocessing)****基于协程的异步I/O(Asyncio)**。以下是它们的核心原理、适用场景及代码示例: --- #### 1. **多线程(Threading)** - **原理**:多个线程共享同一进程的内存空间,通过时间片轮转或并行执行任务。由于Python的全局解释器锁(**GIL**),同一时刻仅一个线程能执行Python字节码[^4]。 - **适用场景**:I/O密集型任务(如文件操作、网络请求)。 - **优点**:轻量级,上下文切换成本低。 - **缺点**:受GIL限制,无法充分利用多核CPU。 ```python import threading def task(): print("Thread running") t = threading.Thread(target=task) t.start() t.join() ``` --- #### 2. **多进程(Multiprocessing)** - **原理**:每个进程拥有独立的内存空间Python解释器,通过操作系统调度实现并行。可绕过GIL限制[^3]。 - **适用场景**:CPU密集型任务(如数值计算、图像处理)。 - **优点**:充分利用多核CPU。 - **缺点**:进程间通信(IPC)成本高,内存占用较大。 ```python from multiprocessing import Process def task(): print("Process running") p = Process(target=task) p.start() p.join() ``` --- #### 3. **异步I/O与协程(Asyncio)** - **原理**:单线程内通过事件循环调度协程,利用非阻塞I/O操作实现高并发。协程通过`async/await`语法挂起/恢复执行[^1]。 - **适用场景**:高并发网络请求(如Web服务、爬虫)。 - **优点**:资源消耗低,适合大规模连接。 - **缺点**:需重构为异步代码,调试复杂。 ```python import asyncio async def task(): print("Coroutine running") async def main(): await task() asyncio.run(main()) ``` --- ### 方法选择建议 | **场景** | **推荐方法** | |-------------------|-------------------| | I/O密集型(如爬虫)| 多线程或异步I/O | | CPU密集型(如计算)| 多进程 | | 高并发网络请求 | 异步I/O(Asyncio)| --- #### **关键区别** - **多线程 vs 多进程**:线程共享内存,进程独立内存;多进程更适合CPU密集型任务[^4]。 - **异步I/O vs 多线程**:异步I/O单线程处理高并发,多线程依赖操作系统调度。 --- ### 典型问题示例 1. **任务并行下载** ```python # 多线程实现 from concurrent.futures import ThreadPoolExecutor def download(url): # 模拟下载逻辑 return f"Downloaded {url}" with ThreadPoolExecutor(max_workers=3) as executor: results = executor.map(download, ["url1", "url2", "url3"]) print(list(results)) ``` 2. **CPU密集型计算** ```python # 多进程实现 from multiprocessing import Pool def compute(n): return n * n with Pool(4) as p: print(p.map(compute, [1, 2, 3])) ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值