Flask-Gevent环境下首次触发Twilio/Pushbullet发送消息遇永久阻塞问题
我最近在开发一个水族箱监控应用,当浮球开关触发时,需要通过Twilio和Pushbullet发送告警通知,但首次触发总会弹出这个gevent错误:
File "/home/pi/simplyfishy/simplyfishyenv/local/lib/python3.5/site-packages/gevent/hub.py", line 630, in switch return RawGreenlet.switch(self) gevent.hub.LoopExit: ('This operation would block forever', <Hub at 0x7451b210 epoll pending=0 ref=0 fileno=17>)
完整调用栈细节就不展开了,核心问题是第一次触发必现这个“会永久阻塞”的错误,但之后再触发开关,两条消息都能正常发送,完全没毛病。我已经试过用gevent.spawn异步调用发送函数,而且猴子补丁代码绝对放在了文件最顶部,还是没搞定首次触发的问题。
先搞懂为啥会出这个错
这个LoopExit本质是gevent的事件循环出现了矛盾:要么它认为自己该终止了,但还有阻塞操作要执行;要么某个操作在事件循环已经终止后才尝试发起阻塞IO。结合“首次触发才炸,后续正常”的情况,十有八九是首次运行时异步任务和事件循环的初始化没配合好——比如事件循环还没完全启动,就发起了阻塞请求;或者任务刚异步启动,主进程没稳住事件循环,导致循环提前退出了。
试试这几个解决方向
1. 明确等待异步任务执行完
如果你的Twilio/Pushbullet发送逻辑是同步IO(比如官方SDK大多是同步的),哪怕用gevent.spawn包了,也要确保事件循环在任务运行期间别提前结束。首次触发时,用gevent.joinall明确等待所有异步任务完成:
import gevent from gevent import monkey monkey.patch_all() # 这行必须放在所有IO相关import之前! def send_twilio_alert(): # 你的Twilio消息发送代码 pass def send_pushbullet_alert(): # 你的Pushbullet消息发送代码 pass def on_float_switch_trigger(): # 首次触发时,把任务都丢进greenlet,然后等它们跑完 alert_tasks = [ gevent.spawn(send_twilio_alert), gevent.spawn(send_pushbullet_alert) ] gevent.joinall(alert_tasks, timeout=10) # 加个超时,防止真的永久阻塞
2. 检查猴子补丁是不是真的生效了
gevent的补丁得覆盖所有可能的阻塞IO模块(比如socket、requests这些),如果你的通知SDK用了什么小众的IO库,或者补丁没完全打上,首次调用就会出问题。可以在补丁之后打印一下被补丁的模块,确认关键模块都在列表里:
from gevent import monkey monkey.patch_all() print(monkey.saved) # 看看哪些模块被替换了,确保requests、socket这些都在
要是发现某个依赖的IO模块没被补丁,就手动单独补,比如monkey.patch_socket()或者monkey.patch_requests()。
3. 给首次触发加个小延迟(临时但好用的调试手段)
有时候首次启动时,系统资源或者SDK的初始化还没完全搞定,异步调用一上来就撞墙。可以在首次触发时加个几百毫秒的延迟,给事件循环和SDK留点初始化时间:
import gevent def on_float_switch_trigger(): gevent.sleep(0.5) # 等0.5秒再启动任务 alert_tasks = [ gevent.spawn(send_twilio_alert), gevent.spawn(send_pushbullet_alert) ] gevent.joinall(alert_tasks)
这招虽然有点“野路子”,但能快速帮你确认是不是初始化时机的问题。
4. 确保事件循环全程不退出
如果你的应用是单线程运行的,首次触发后事件循环意外退出,后续触发时可能因为全局状态已经初始化了所以正常。可以在主进程里加一行,让事件循环一直跑着:
from gevent.hub import get_hub # 主逻辑写完后,加这行阻止主进程退出,直到所有任务完成 get_hub().join()
总结来说
这个问题的核心就是首次运行时异步任务和gevent事件循环的协同出了问题,重点查三个点:补丁是不是全覆盖了、任务有没有被正确等待、初始化时机对不对。按照上面的步骤挨个试,应该能把首次触发的这个LoopExit错误干掉。
内容的提问来源于stack exchange,提问作者DexDeadly

