Flask中使用gevent.spawn出现Popped wrong app context错误的解决咨询
这个问题的核心在于@copy_current_request_context装饰器的作用时机和请求上下文的生命周期冲突了,咱们一步步拆解原因和解决方案:
为什么原代码会报错?
你在f1函数内部定义了_test并使用@copy_current_request_context装饰——这个装饰器会在定义_test的那一刻就捕获当前的请求上下文并生成一个副本。当你用gevent.spawn启动两个协程时,这两个协程共用的是同一个上下文副本。
当第一个耗时5秒的协程执行完毕,它会弹出这个共享的上下文副本;而第二个耗时10秒的协程还在运行,等它执行完要弹出上下文时,发现上下文栈里的内容和它预期的不一致,就会抛出"Popped wrong app context"错误。更关键的是,当视图函数f1返回后,Flask会自动清理当前请求的上下文,这会进一步干扰协程里的上下文状态。
为什么_test_bis能正常工作?
因为_test_bis是定义在全局作用域的,每次调用它(也就是gevent.spawn启动协程的时候),@copy_current_request_context都会重新捕获当时的请求上下文,生成一个独立的副本给每个协程。这样两个协程的上下文完全分离,各自执行完毕后弹出自己的上下文,不会互相干扰。
解决方案
方案1:把协程函数移到全局作用域(推荐)
和_test_bis的思路一致,把需要并发执行的函数定义在视图函数外面,确保每个协程都能获取独立的上下文副本:
from flask import copy_current_request_context import requests import time @copy_current_request_context def test_coroutine(t): time.sleep(t) r = requests.get( 'https://webhook.site/d14a41a6-9c6a-4da0-bbe6-3cc660daea3d', params=dict(word='test') ) return r.status_code def f1(self): jobs = [gevent.spawn(test_coroutine, 5), gevent.spawn(test_coroutine, 10)] results = [job.value for job in gevent.joinall(jobs)] return None
方案2:手动管理请求上下文
如果必须在视图函数内部定义协程函数,可以手动复制上下文并在协程里用with语句管理生命周期,避免共享副本的问题:
def f1(self): # 手动捕获当前请求上下文的副本 current_ctx = copy_current_request_context(lambda: None).__closure__[0].cell_contents def _test(t): with current_ctx: time.sleep(t) r = requests.get( 'https://webhook.site/d14a41a6-9c6a-4da0-bbe6-3cc660daea3d', params=dict(word='test') ) return r.status_code jobs = [gevent.spawn(_test, 5), gevent.spawn(_test, 10)] results = [job.value for job in gevent.joinall(jobs)] return None
这里我们手动复制了请求上下文,每个协程通过with语句独立使用这个副本,执行完毕后会自动弹出上下文,不会互相干扰。
额外注意事项
- 确保
monkey.patch_all的调用时机正确(你的代码里已经在导入Flask和requests之前调用,这点没问题)。 - 不要在协程里依赖Flask的
g对象做跨协程状态共享,因为每个协程的上下文是独立的。
内容的提问来源于stack exchange,提问作者Jeremy D

