在进行flask应用开发的时候遇到这样的一个问题:
在主线程之外需要一个异步处理的事件,需要反复启动和运行该函数(事件)
现在做一个比喻来理解这个问题
Flask主线程:是厨房的主厨。他负责接待所有顾客的点单(HTTP请求),并且快速地处理一些简单的菜,比如做个沙拉(返回静态页面)。他非常忙,必须马上响应下一个顾客,不能被一道菜卡住。
某一个函数:这是一道非常复杂的“佛跳墙”。它需要启动一个子进程(点燃一个新的炉子)、和炉子上的锅(服务器)来回沟通好几次。做这道菜要花很长时间。
为了解决这个问题,我们雇佣了一位副厨,并给了他一个专用的炉子。主厨以后接到“佛跳墙”的订单,就直接交给副厨去做。
首先,需要购买一个新的炉子(新的进程)
background_loop = asyncio.new_event_loop()
然后,需要写一份说明书
def start_event_loop(loop: asyncio.AbstractEventLoop):
"""在一个线程中启动并永远运行事件循环"""
# a. 将当前线程的事件循环设置为我们传入的那个loop对象
asyncio.set_event_loop(loop)
# b. 启动事件循环,并让它永远运行下去,等待任务。
loop.run_forever()
- asyncio.set_event_loop(loop):“到岗后,请认准分配给你的那个炉子(loop),以后你只用它。” (这确保了副厨不会和主厨抢炉子用)
- oop.run_forever():“点燃你的炉子,然后就守在旁边,永远不要离开。随时准备接收任务。” (这就是让炉子一直“运行中”的关键)
正式雇佣副厨,让他上岗工作
# 创建一个全局的事件循环和后台线程
loop_thread = threading.Thread(
target=start_event_loop, # 告诉副厨,你的工作内容是那份“岗位说明书”
args=(background_loop,), # 把“专用的新炉子”交给他
daemon=True # 告诉他,如果主厨下班了,你也跟着下班
)
loop_thread.start() # 命令副厨:“开始工作!”
- threading.Thread: 我们从人才市场(threading库)找到了一个人,准备让他成为我们的新员工 (loop_thread)。
- target=start_event_loop: 我们把“岗位说明书”交给了他。
- args=(background_loop,): 我们把“专用的新炉子”指派给了他。
- daemon=True: 这是一个重要的雇佣条款。意思是“你是一个临时工,只要我这个主厨(主程序)下班关店了,你就可以立刻下班,不用管手里的活干完没”。这能防止主程序退出后,后台线程还在运行的尴尬情况。
- loop_thread.start(): 主厨一声令下,副厨立刻跑到他的炉子边,按照岗位说明书的要求,点火并永远地守在那里了。
那么应该怎么使用呢:举例:
def handle_message():
# ...
# a. 把 “做佛跳墙” (ai_call) 这个任务,安全地提交给副厨的炉子
future = asyncio.run_coroutine_threadsafe(ai_call(user_input), background_loop)
# b. 主厨在这里拿着“取餐单”(future),等待副厨完成。
response = future.result()
- 语法 asyncio.run_coroutine_threadsafe(...):
- 比喻:这是主厨的一个特殊技能,叫做“线程安全的任务交接”。他把“佛跳墙的菜谱”(ai_call(user_input)),贴在了副厨的炉子(background_loop)上,并拿到了一张取餐单(future)。
- 语法 future.result():
- 比喻:主厨拿着这张取餐单,就在出餐口等着。他自己不干活,就只是等待。这个等待是阻塞的,但他只阻塞了“上这一道菜”这个行为,他随时可以被其他事情打断(尽管在Flask里他还是会等完)。一旦副厨把菜做好了放在出餐口,future.result() 就立刻拿到了这道菜(response),主厨就可以把它端给客人了。