异步编程很棒,特别是对于构建可扩展应用程序感兴趣的后端开发人员。在实践中,
它是构建高度并发服务器的最重要的工具之一。
但现实是痛苦的。许多处理 I/O 繁忙问题的流行包并没有使用异步代码。主要原因是:
• Python 3 及其一些高级功能的采用率依然很低。
• Python 初学者对各种并发概念的理解不足。
这意味着,迁移现有的同步多线程应用程序和软件包要么不可能(由于架构的限制),要
么代价太大。许多项目可以从并入异步风格的多任务中获益匪浅,但只有少数人最终会这样做。
这意味着,现在,从一开始尝试构建异步应用程序时,你就会遇到很多困难。在大多数情
况下,这些困难类似于异步编程实例中提到的问题—不兼容接口和 I/O 操作的非异步阻塞。
当然,当你遇到这种不兼容性,并且只是同步获取所需的资源时,你有时可以不使用
await。但是在你等待结果时,这将阻止每个其他协程在执行它的代码。它技术上工作,
但也破坏了异步编程的所有收获。因此,最后,加入异步 I/O 与同步 I O 不是一个选项。它
是一种全或无的游戏。
另一个问题是长时间运行 CPU 密集型操作。当你执行 I/O 操作时,可以很容易地从协
程释放控制。当从文件系统或套接字写/读时,你最终会等待,所以使用 await 调用是最
好的。但是,当你需要实际计算的东西,你知道这将需要一段时间做什么?你当然可以把
问题分解成部分和释放控制每次你向前移动一点。但你会很快发现这不是一个好的模式。
这样的事情可能使代码混乱,也不能保证良好的效果。分词应由解释器或操作系统负责。
所以如果你有一些代码,长时间进行同步 I/O 操作,你不能或不愿意重写,该怎么办。
或者当你在主要使用异步 I/O 设计的应用程序中进行一些繁重的 CPU 密集型操作时该怎么
办?你需要使用解决方法。而解决方法我的意思是多线程或多处理。
这可能听起来不太好,但有时最好的解决方案可能是我们试图逃避的。在 Python 中并
行处理 CPU 广泛的任务总是用多进程处理更好。多线程可以处理 I/O 操作同样好(快速,
没有大量的资源开销)as async 和 await,如果正确设置和小心处理。
因此,有时你不知道该怎么做时,当某些东西根本不适合你的异步应用程序时,使用
一段代码,将它推迟到单独的线程或进程。你可以假装这是一个协程,释放控制到事件循
环,最终在准备好时处理结果。幸运的是,Python 标准库提供了 concurrent.futures
模块,它也与 asyncio 模块集成。你可以使用这两个模块一起调度在线程或其他进程中执行的阻塞函数,因为它是同步非阻塞协同。
Executors 与 futures
在我们看到如何将线程或进程注入到异步事件循环之前,我们将进一步了解
concurrent.futures 模块,该模块稍后将会是我们所谓的解决方法的主要组成部分。
concurrent.futures 模块中最重要的类是 Executor 和 Future。
Executor 表示可并行处理工作项的资源池。这看起来非常类似于来自multiprocessing
模块-Pool 和 dummy.Pool 的类—但是具有完全不同的接口和语义。它是一个不用于实
例化的基类,它有两个具体的实现。
• ThreadPoolExecutor:这代表线程池。
• ProcessPoolExecutor:这代表进程池。
每个执行者提供 3 个方法。
• submit(fn, * args, ** kwargs):这将在资源池上执行调度 fn 函数,并返
回 Future 对象,该对象表示可调用的执行。
• map(func, * iterables, timeout = None, chunksize = 1):在一个
迭代器上执行 func 函数,它的方式类似于 multiprocessing.Pool.map()方法。
• shutdown(wait = True):这将关闭执行程序并释放其所有资源。
最值得注意的方法是 submit(),因为它返回 Future 对象。它表示一个可调用的
异步执行,只是间接表示其结果。为了获得提交的可调用的实际返回值,你需要调用
Future.result()方法。如果可调用已经完成,result()方法将不会阻塞它,只会返
回函数输出。如果不是真的,它将阻塞,直到结果准备好。把它看作一个结果的承诺(实
际上它是一个与 JavaScript 中的 promise 相同的概念)。你不需要在收到它之后立即解开它
(使用 result()方法),但是如果你试图这样做,它保证最终返回结果如下:
def loudy_return():
… print(“processing”)
… return 42
…
from concurrent.futures import ThreadPoolExecutor
with ThreadPoolExecutor(1) as executor:
… future = executor.submit(loudy_return)
…
processing
future
<Future at 0x33cbf98 state=finished returned int>
future.result()
42
如果你想使用 Executor.map()方法,它与 multiprocessing 模块中的 Pool 类
的 Pool.map()方法的用法没什么不同,如下所示:
def main():
with ThreadPoolExecutor(POOL_SIZE) as pool:
results = pool.map(fetch_place, PLACES)
for result in results:
present_result(result)