探索 Flask 对 asyncio 的支持

源于自己折腾的一个小 Flask 项目中,后台需访问多个 HTTP 服务,目前采用 ThreadPoolExecutor 多线程的方式处理的。但因访问 HTTP 服务有前后关联关系,如得到请求 A 的结果后再访问 B,这似乎用 Promise.then().then() 编程方式更合适些。于是巡着这一路子,翻出 Python 的各种相关部件来,比如 Python 对 coroutine(协程) 的支持,asyncio, 及后面的 async/await 关键子,aiohttp 组件,requests 的 async 替代品有 aiohttp, grequests, 和 httpx,aiohttp  可替代 Flask, 最后竟然找到了一个更彻底的 Flask 的 Async 版本 Quart。 

Python 3.4 引入了 asyncio 模块,基于生成器(yield 和 yield from) 和 @asyncio.coroutine 的方式来支持 coroutine(协程), 到 Python 3.5 后有了 async/await(@asyncio.corouting 替换为 async, yield from 替换为 await) 关键字,协程的实现变得更为简单。Python 3.4  使用 coroutine 的方式我们跳过,直接看

async/await 方式的实现

执行后输出如下:

2: 2.000903844833374 - MainThread
4: 4.000842094421387 - MainThread
results: [3, 5]
Total elapsed time 4.001974821090698

compute() 函数暂停输入参数的秒数,分别两个任务,暂停时间各自为 2 和  4  秒,但总的执行时间为最大的那个数字,相当于那两个任务是并发执行的。注意,我们这里并没有使用到线程,都是用的 MainThread,却收到同样的效果

自 Python 3.7 及之后可以用  asyncio.run() 来简单调用,以上的代码从 tasks = ... 行开始可替换为如下代码

执行后获得相同的结果。

我们查看一下  asyncio.run()  函数的源代码

Python 3.4 开始,在主线程上可以用 asyncio.get_event_loop() 直接获得 EventLoop,主线程上存在 EventLoop 直接返回,无则创建新的。 而 asyncio.main() 方法总是创建一个新的 EventLoop

Flask 中实现一个异步 API

先用 asyncio.run() 的方式,用协程来异步调用三个 URL,分别获得它们的响应文本的长度

python app.py 启动后,访问 / API

curl http://localhost:5000/
response sizes: [12019, 60030, 96362]

由前所知 asyncio.run() 总是会在当前线程上创建并注册一个 EventLoop,所以它总是可行的。那么能不能直接用 asyncio.get_event_loop() 获得一个 EventLoop 呢?

index() 方法中用 asyncio.get_event_loop(),报错

RuntimeError: There is no current event loop in thread 'Thread-6'.

就是说在 Flask 启动的处理 HTTP  的线程上没有 EventLoop,而每次都需要自己注册一个。从每次的线程来看,Flask 应该是每次请求都创建一个线程来处理任务,用 get_event_loop() 方式的代码如下:

每次请求都会打印 create a new EventLoop

虽然用 asyncio.run() 避免了每次创建并注册新 EventLoop 的过程,但有时候我们确实需要 EventLoop 的方法处理协程,这样使用 Flask 的异步方式就稍显麻烦。

异步调用使用主线程的 EventLoop

我们知道,Python 的主线程上注册有一个  EventLoop,所以我们可以让所有异步调用用主线程上那个 EventLoop, 以下代码来自于 Python3 Asyncio call from Flask route

异步调用全部用主线程上的 EventLoop

Quart 作者提供的一个 Flask async 方案

Making Flask async and Quart sync, Quart 的作者 PG Jones 给出了一个 Flask 异步化的代码,route 方法可加上 async  关键字和 @run_async 装饰

启动后,测试下

$ curl localhost:5000
"hello"

两个不在维护的 Flask 扩展

另外还有两个试图扩展 Flask 异步功能的已不再维护的组伯

Flask-aiohttp,已经找不到怎么安装它。GitHub 的代码 Flask-aiohttp 已不再维护,实现上加了一个 @async 装饰器

Flask-Async, 在 PyPi 上有,也是一个比 Flask-aiohttp 更久远的项目,最近修改 6 年前,它是一个 Flask 的修改版本,加入了异步特性。实现上与 Flask-aiohttp 类似,只不过它的装饰器是 @coroutine

看得出来它还没用上 Python 3.5 的 async/await 关键字来实现协程。

使用 aiohttp 进行异步 HTTP 调用

requests 是一个同步 HTTP 请求库,为了应用到协程当中去,必须把请求包装到 async def 定义的方法中去。aiohttp 提供了异步的方法,aiohttp 库同时提供了服务端和客户端,服务端可以用来替代 Flask  功能,可启动 HTTP 服务并用路由来定义不同的 API。我们这里只使用它的客户端组件,

安装 aiohttp

$ pip install aiohttp

下面用 aiohttp 代替前面的 fetch(url) 方法

想要实现 Promise 那样的 then/then 功能,没找到  aoihttp 现成的方法,准确说是 Python 的  coroutine 没提供像 Java 的 CompletableFuture 那样完备的 thenRun(), thenApply() 等等方法,所以在 Python  中还得自己用 async/await 关键字串起来,比如基于第一个异步请求的响应数据,发现第二个异步请求

再继续 Google 找啊找啊,可以直接用 aiohttp 来实现异步的 Web 服务器,再进一步 Quart 是一个比  Flask 更完美的替代器。原本写在一篇博客之中,最后还是决定另立新篇来介绍 aiohttp 和 Quart 实现 异步 Web 服务器。

总结:

综合前面,我们认识到

  1. Python 要支持协程必须要与 EventLoop 交互
  2. 自 Python 3.4 之后,主线程上可用 asyncio.get_event_loop() 直接获得 EventLoop
  3. asyncio.run() 总是创建一个新的 EventLoop,然后协程在其中执行地
  4. Flask 处理请求时每次都创建一个新的线程,该线程上没有注册 EventLoop
  5. 因为上一条,在 Flask(当前版本 1.1.2), 要使用协程,必须每次注册一个 EventLoop, 用 asyncio.run() 或用如下两行代码获得 EventLoop
           loop = asyncio.new_event_loop()
           asyncio.set_event_loop(loop)
  6. 如果要用一个异步版的 requests, 可以选 aiohttp, grequests, 或 httpx
  7. 可替代的 aiohttp 服务组件和 Quart 可直接支持协程,因为它们的路由方法可用 async 修饰,首选  Quart

链接:

  1. Async IO in Python: A Complete Walkthrough
  2. Python3 Asyncio call from Flask route

 

类别: Python. 标签: . 阅读(52). 订阅评论. TrackBack.
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x