Python asyncio call_soon:阻塞与非阻塞调用混合执行的疑问
Asyncio call_soon 执行行为解析
核心问题解答
1. 为何第一段代码总时长为3秒而非4秒?
这是因为asyncio的事件循环是单线程运行的,且call_soon注册的同步回调会在协程让出控制权后立即执行,同步阻塞代码会卡住整个事件循环,直接覆盖了异步sleep的等待时间。具体执行流程如下:
- 事件循环启动
amain协程,调用loop.call_soon(blocking)将blocking函数加入即时回调队列。 amain执行await nonblocking(),进入nonblocking协程:- 打印
nonblocking called,随后执行await asyncio.sleep(1)——这会向事件循环注册一个1秒后的定时唤醒任务,同时nonblocking协程暂停,让出控制权给事件循环。
- 打印
- 事件循环检查定时任务尚未到期(刚注册不足1秒),转而处理即时回调队列中的
blocking函数:blocking开始执行,time.sleep(3)是同步阻塞操作,会完全卡住事件循环,期间无法处理任何异步任务(包括那个1秒后的定时唤醒)。
- 3秒后
blocking执行完毕,事件循环重新检查定时任务,发现asyncio.sleep(1)的唤醒时间早已过期,立即恢复nonblocking协程,打印nonblocking done后协程结束。 amain协程结束,事件循环关闭。
整个过程中,blocking的3秒阻塞完全覆盖了nonblocking中1秒的异步等待,因此总耗时为3秒。
2. call_soon在此场景下的实际作用是什么?
loop.call_soon(func)的核心作用是向asyncio事件循环注册一个同步回调函数,该函数会在事件循环的下一个“可执行回调”时机(即当前协程让出控制权后)被同步执行。
需要重点注意两点:
- 回调是同步执行的:如果回调包含
time.sleep()这类阻塞操作,会直接卡住整个事件循环,所有异步任务都会暂停,直到回调执行完毕。 - 回调优先级高于普通协程任务:事件循环在处理完到期的定时任务和IO事件后,会优先执行
call_soon注册的回调,再去处理任务队列中的协程。
额外场景解析
场景1:替换为await asyncio.gather(nonblocking(), nonblocking(), nonblocking())后总时长4秒
当使用gather同时调度3个nonblocking协程时,执行流程变为:
call_soon(blocking)将回调加入队列后,gather启动3个nonblocking协程,每个协程都执行到await asyncio.sleep(1)并暂停,注册3个1秒后的定时任务。- 事件循环等待1秒,直到3个定时任务到期,依次唤醒3个
nonblocking协程,完成后gather结束。 - 事件循环此时才处理
call_soon注册的blocking回调,执行3秒的同步阻塞。
总耗时为1秒(异步等待)+3秒(同步阻塞)=4秒,核心原因是事件循环会先处理完所有到期的定时任务,再执行call_soon的回调。
场景2:添加第二个loop.call_soon(blocking)后,先执行第一个blocking和nonblocking,3秒后执行第二个blocking
call_soon注册的回调是按顺序排队执行的:
- 两个
blocking函数被依次加入即时回调队列。 - 当
nonblocking协程让出控制权后,事件循环先执行队列中的第一个blocking,耗时3秒。 - 第一个
blocking执行完毕后,事件循环处理nonblocking的定时任务(已过期),完成nonblocking协程。 - 最后事件循环才执行队列中的第二个
blocking函数。
这是因为事件循环会在处理完当前回调后,再依次处理队列中的下一个回调,且同步回调的执行会阻塞整个循环,必须等前一个回调完成才能执行下一个。
内容的提问来源于stack exchange,提问作者Charlie Shuffler
相关产品推荐
相关产品推荐

