使用%autoawait时await阻塞Jupyter单元格响应ipywidget事件的方案
问题根因
该现象是Jupyter内核的默认执行模型导致的:单元格代码运行(包括await挂起等待I/O)期间,内核不会主动派发前端发来的widget trait变更消息,所有消息会暂存在队列中,直到单元格执行完成后才会批量处理,因此你在await阶段无法收到traitlet事件。
可行实现方案
不需要将回调放到后台执行,你可以在等待交互事件的间隙主动触发内核处理消息队列,完全符合trio的结构化并发设计原则,也不需要额外梳理后台任务逻辑。修改后的代码如下:
import trio from IPython import get_ipython from IPython.display import display from ipywidgets import IntSlider, Output %autoawait trio async def wait_for_change(widget, attribute): result = None evt = trio.Event() kernel = get_ipython().kernel def value_changed(change): nonlocal result result = change evt.set() widget.observe(value_changed, attribute) async def poll_kernel(): # 每50ms处理一次内核消息队列,直到事件触发 while not evt.is_set(): kernel.do_one_iteration() await trio.sleep(0.05) try: async with trio.open_nursery() as nursery: nursery.start_soon(poll_kernel) await evt.wait() nursery.cancel_scope.cancel() finally: widget.unobserve(value_changed, attribute) return result slider = IntSlider() out = Output() display(slider) display(out) for i in range(10): out.append_stdout('did work ' + str(i) + '\n') x = await wait_for_change(slider, 'value') out.append_stdout('async function continued with value ' + str(x) + '\n')
方案说明
- 新增的
poll_kernel任务会周期性调用kernel.do_one_iteration()处理所有暂存的前端消息,widget的变更回调会在这个阶段正常触发,唤醒等待的事件 - 用trio nursery管理任务,事件触发后立即取消轮询任务,没有残留的后台任务,完全符合结构化并发的设计要求
- 轮询间隔可根据需求调整,50ms的间隔不会产生可感知的交互延迟,也不会占用过多CPU资源
- 该方案也适配asyncio场景,仅需要将trio的并发、睡眠API替换为asyncio对应实现即可
内容的提问来源于stack exchange,提问作者burnpanck
相关产品推荐
相关产品推荐

