本来只是为了研究一下 Flask 怎么去支持早已在 Python 的支持的 coroutine 功能,没想步子越迈越大,直顶到 aiohttp Web 服务器和 Flask 的异步实现版本 Quart。Flask 得费了好一番功夫去获得 EventLoop
,可知 aiohttp 和 Quart 的路由方法直接就允许 async
的,那个 EventLoop
自然就在其中。从 async
的路由方法出发去调用别的异步方法就是一件十分轻松的事情。
下面来稍稍体验一下用分别用 aiohttp 和 Quart 实现简单的异步服务器,我们的关注点在它的异步路由。
异步的 aiohttp Web 服务器
aiohttp 除了 HTTP 客户端功能,还有服务端端,因它的异步特性,可以用它建立一个异步的 Web 服务器,也就是它的路由方法也是异步的,完全可用它来替代 Flask 本身。
用 pip install aiohttp
安装
下面是一个简单的例子
1 2 3 4 5 6 7 8 9 10 11 12 |
from aiohttp import web import asyncio import threading async def hello(request): loop = asyncio.get_event_loop() thread_name = threading.current_thread().name return web.json_response({'event_loop': str(loop), 'thread': thread_name}) app = web.Application() app.add_routes([web.get('/', hello)]) # 只能注册 async 修饰的方法 web.run_app(app) |
由于 hello() 方法加了 async
关键字,可直接由路由 /
关联来执行,所以它是有一个 EventLoop
在里头的,加了一行代码在控制台打印出该 EventLoop
。同是我们也来观察一下它用什么线程来处理客户端请求。aiohttp 服务启动的默认端口号是 8080
======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)
发出请求
$ curl http://localhost:8080/
{"event_loop": "<_UnixSelectorEventLoop running=True closed=False debug=False>", "thread": "MainThread"}
比起 Flask 应用 coroutine 时不需要显式的用 asyncio.run()
或用下面几行代码
1 2 3 4 |
loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(asyncio.gather(tasks)) |
不过 aiohttp
的服务总是用主线程去处理客户端请求,那就是说使用 aiohttp
做服务器的话,需要各个层所有的方法都是 async
的,这样多个请求之前才不至于互相阻塞。同时 aiohttp
用 app.add_routes()
注册路由时,只能支持 async
修饰的方法,也就是说 asiohttp
不支持非异步的方法。这要求后端的所有实现方法必须小心,一旦调用了非异步方法将阻塞其他的请求。
Flask 的超集版本 Quart
Quart 直接被定义为 Flask 的超集,支持异步路由,使用了 Flask 的 API,支持 Flask 的扩展,还添加了一些 Flask 不具备的功能。Quart 当前版本 0.13.0,它从 0.7.0 开始需要 Python 3.7.0 或更高版本的。
用 pip install quart
安装
下面来看熟悉的味道,同样测试 async
和 非 async
两个路由方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
from quart import Quart, jsonify import asyncio import threading app = Quart(__name__) @app.route('/async') async def hello_async(): loop = asyncio.get_event_loop() thread_name = threading.current_thread().name return jsonify({'event_loop': str(loop), 'thread': thread_name}) @app.route('/sync') def hello_sync(): loop = None try: loop = asyncio.get_event_loop() except RuntimeError: pass thread_name = threading.current_thread().name return jsonify({'event_loop': str(loop), 'thread': thread_name}) app.run(debug=True) |
python app.py
启动它,同样是监听在 5000
号端口
Running on http://127.0.0.1:5000 (CTRL + C to quit)
[2020-07-13 11:26:01,137] Running on 127.0.0.1:5000 over http (CTRL + C to quit)
访问及结果
$ curl http://localhost:5000/async
{
"event_loop": "<_UnixSelectorEventLoop running=True closed=False debug=True>",
"thread": "MainThread"
}
$ curl http://localhost:5000/sync
{
"event_loop": "None",
"thread": "ThreadPoolExecutor-0_0"
}
同样,既然是路由方法上可用 async
关键字,自然它在执行时能拿到当前的 EventLoop
,调用其他的 async
方法不在话下。与 aiohttp
一样,async
的路由方法总是由主线程来处理请求。非 async
的路由方法由线程池来处理,这比 Flask 每请求创建一个新的线程要先进一些。
Quart 同时支持异步和非异步的方法,这给了我们更多的灵活性,比如使用非 async
路由方法时,某些地方我们可以手动的用 EventLoop
来调度,而不一定要求一切 async。
轻松搞上 websocket
Flask 支持 websocket 需要安装一个 flask-socketio 扩展,而 Quart 更简单,有装饰器支持
1 2 3 4 5 6 |
from quart import websocket @app.websocket('/ws') async def ws(): while True: await websocket.sned('hello') |
值得一试
一个获得 EventLoop
的工具方法
对于 Quart 中异步或同步路由中都想获得 EventLoop
进行更精细的方法调度,可以用下面的工具方法来获得或创建一个新的 EventLoop
1 2 3 4 5 6 7 8 9 10 |
def get_event_loop(): try: event_loop = get_event_loop() except RuntimeError: event_loop = asyncio.new_event_loop() asyncio.set_event_loop(event_loop) if event_loop.is_running(): raise RuntimeError("called from a running event loop") return event_loop |
线程上存在直接返回,否则创建一个 EventLoop
, 如果是同步路由方法,必须自己用 event_loop.run_until_complete(...)
发起协程的执行,相当于进行 Promise 的最终兑现。
总结一下:
- aiohttp 只支持
async
路由方法,所有请求都在主线程中处理,任何非异步方法的调用都将阻塞其他的请求 - Quart 同时支持
async
和非async
路由方法,async
路由由主线程处理,这一点与aiohttp
的路由是一样的。 - Quart 的非
async
路由方法由线程池处理,比 Flask 每次请求新建线程要好 - Quart 允许我们同时用非
async
路由与EventLoop
来控制 - 在 aiohttp 或 Quart 中使用
async
路由时反而要倍加小心,最好是所有方法都是async
的
本文链接 https://yanbin.blog/flask-replacement-aiohttp-quart/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。