Flask是一个同步的,使用WSGI协议的Python WEB框架,他不能和sanic,fastapi那样使用异步的事件循环来处理请求,也不能利用uvicorn等的ASGI服务器来加速自己,耗时任务需要发布到Celery来执行。
因为在Flask中遇到了耗时的任务和代码,需要阻塞等待,绝大部分任务都分发到了Celery中,统一处理。所以有了在同步服务器中使用异步代码来处理任务,来看对现有程序的影响。
这里提供一种思路,在flask项目中,使用asyncio模块,来实现异步任务。主要是利用了asyncio的future对象,发布异步执行的命令之后,方法会在异步的executor中执行,不阻塞当前的代码出行。
先说结论
- 使用线程池或者进程池executor或者直接发布,python会在非阻塞的环境中执行这段代码
- 因为loop没有对发布的event进行管理(我是这么理解的),不监控事件的执行状态,不能实现callback回调
- 使用了线程共享某些对象的方法,例如future对象,实现了任务执行完毕,执行指定代码
在这里记录一下我的思路,详细的代码会在末尾给出。
首先是创建一个flask的app对象,给这个对象一个事件循环,让他用这个时间循环来发布异步的任务。
#! /usr/bin/env python
# A test for asyncio usage in flask app
import asyncio
from flask import Flask, current_app, jsonify, request
from loguru import logger
from works.dummy_work import delay, fake_work
app = Flask(__name__)
if __name__ == "__main__":
app.event_loop = asyncio.get_event_loop()
try:
app.run(debug=True)
except Exception as e:
logger.error(f'Error happened: {e}')
finally:
app.event_loop.stop()
app.event_loop.run_until_complete(app.event_loop.shutdown_asyncgens())
app.event_loop.close()
模拟一个耗时的任务
def fake_work(*args, **kwargs):
"""
Simulate a long time cost work
"""
logger.info('In fake_work function')
logger.info(f'Args: {args}')
time.sleep(10)
logger.info(f'Finished fake work')
一个发布异步任务的方法
def delay(func: typing.Callable, app: Flask = None, *args, **kwargs):
"""
Publish a future async work
"""
assert callable(func), "``func`` param has to be a callable function"
future = current_app.event_loop.run_in_executor(
None, func, *args
)
future.add_done_callback(callback)
logger.info('Add callback function to future obj')
一个回调函数,来测试执行成功后,能否实现回调
def callback(future: asyncio.Future, *args, **kwargs):
"""
async function callback
"""
logger.info(f'Passed future object is {future}')
然后在接口中调用发布任务的逻辑
@app.route('/test', methods=['GET'])
def index():
delay(fake_work, current_app, num=request.args.get('num'))
return jsonify({"msg": "ok"})
启动服务
> flask run
用httpie进行测试
> http :5000/test
HTTP/1.0 200 OK
Content-Length: 13
Content-Type: application/json
Date: Thu, 18 Jun 2020 07:38:05 GMT
Server: Werkzeug/1.0.1 Python/3.7.5
{
"msg": "ok"
}
fake_work在sleep了10秒之后,打印了Finish fake work,但是没有回调。
可以看到实现了异步任务的发布和执行,为什么使用run_in_executor呢,因为一个异步的函数,在变成coroutine之后,内部的逻辑是不会执行的,只有在event loop接管这个coroutine之后,内部的逻辑才会执行,而同步的代码,比如上面的fake_work,让其被其它executor执行之后,内部的逻辑会继续执行,实际上,他还是一个同步的代码,会直接执行,但是,event loop将其交给了executor来执行,这里这个参数是None,实际上会调用默认的执行器,concurrent.futures.ThreadPoolExecutor对象,然后交给他来处理。
因为这是由event loop来发布的,所以你也可以等待他执行完,loop.run_until_complete(future),使用这个方法还可以实现回调,但是会阻塞等待任务的执行,那就和我们的初衷不一样了。
其实看到时间循环的默认executor对象,我们就可以摆脱asyncio,直接使用executor来执行这个任务。
将发布异步任务的方法改为
def delay(func: typing.Callable, app: Flask = None, *args, **kwargs):
"""
Publish a future async work
"""
assert callable(func), "``func`` param has to be a callable function"
executor = ThreadPoolExecutor(1)
# 也可以是一个进程池
# executor = ProcessPoolExecutor()
executor.submit(func, *args)
实验结果在线程执行完成后,会自动退出,回收
因为事件循环不能监控任务的进度状态,所以不能实现回调。
在任务最后调用其他逻辑,这里有一个问题,如果你需要把其他参数,或者一个在任务更高层的代码的数据,可以在线程间共享数据,使用全局变量,或者使用一个队列来通信。这里用队列来做
def delay(func: typing.Callable, app: Flask = None, *args, **kwargs):
"""
Publish a future async work
"""
assert callable(func), "``func`` param has to be a callable function"
# 默认会创建一个未传入参数的ThreadPoolExecutor,这里指定一个线程数
executor = ThreadPoolExecutor(1)
q = Queue(1)
# 使用全局变量也可以在线程间共享
# global future
future = current_app.event_loop.run_in_executor(
executor, func, *[q]
)
# executor.submit(func, *[q])
q.put(future)
在发布的线程任务中获取这个变量
def fake_work(*args, **kwargs):
"""
Simulate a long time cost work
"""
logger.info('in longcost work function')
logger.info(f'args: {args}')
time.sleep(10)
q = args[-1]
# 如果future是一个全局变量,可以在这里直接使用,而不用在队列里面获取
future = q.get()
callback(future)
# del future
需要注意的一点是,无论任何方式,发布的非阻塞程序,都是在flask的请求生命周期之外的,首先不能使用flask的全局对象request,g和session,也不能使用app.app_context()来创建上下文环境,都会抛出RuntimeError异常。在非阻塞的程序中出现异常,需要单独捕获,可能不会在日志或控制台输出相应的错误提示,在debug时可以看到错误。
因为是创建线程去做了相应的工作,我使用了gunicorn来启动flask,模拟并发请求,并监控线程数,没有发现明显的隐患。后续会继续进行测试。
# works/dummy_work.py
import asyncio
import time
import typing
from concurrent.futures import ThreadPoolExecutor
from queue import Queue
from flask import Flask, current_app, request, g
from loguru import logger
def callback(future: asyncio.Future, *args, **kwargs):
"""
async function callback
"""
logger.info(f'passed future object is {future}')
del future
def delay(func: typing.Callable, app: Flask, *args, **kwargs):
"""
Publish a future async work
"""
assert callable(func), "``func`` param has to be a callable function"
# global future
executor = ThreadPoolExecutor(1)
q = Queue(1)
# future = current_app.event_loop.run_in_executor(None, func, *[1, 2, 3], **kwargs)
future = current_app.event_loop.run_in_executor(
executor, func, *[kwargs['num'], q]
)
executor.submit(func, *[q])
q.put(future)
def fake_work(*args, **kwargs):
"""
Simulate a long time cost work
"""
logger.info('in longcost work function')
logger.info(f'args: {args}')
time.sleep(10)
try:
with current_app.app_context():
logger.info(f'Request obj after a request life circle: {request}')
logger.info(f'G obj after a request life circle: {g}')
except RuntimeError:
pass
logger.info('finished longcost work')
logger.info(f'args: {args}')
q = args[-1]
future = q.get()
callback(future)
# app.py
#! /usr/bin/env python
# A test for asyncio usage in flask app
import asyncio
import threading
from flask import Flask, current_app, jsonify, request
# from flask import g, request
import os
import sys
from loguru import logger
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print(f'base dir {BASE_DIR}')
sys.path.append(f'{BASE_DIR}/app_async')
from works.dummy_work import delay, fake_work
app = Flask(__name__)
@app.route('/test', methods=['GET'])
def index():
"""
Test api
"""
delay(fake_work, current_app, num=request.args.get('num'))
# delay(async_work, current_app, num=request.args.get('num'))
return jsonify({"msg": "ok"})
@app.route('/t')
def thread_count():
count = len(threading.enumerate())
return jsonify({'count': count})
app.event_loop = asyncio.get_event_loop()
if __name__ == "__main__":
# app.event_loop = asyncio.get_event_loop()
try:
app.run(debug=True)
except Exception as e:
logger.error(f'Error happened: {e}')
finally:
app.event_loop.stop()
app.event_loop.run_until_complete(app.event_loop.shutdown_asyncgens())
app.event_loop.close()