如何将阻塞函数转为Python可等待函数?两种实现方案孰优孰劣?
问题描述
我有如下阻塞函数:
def blocking_function(): doing.start_preparing() while not doing.is_ready(): pass return doing.do()
我希望将其改造为异步函数,查阅asyncio源码后找到了两种实现方案:
import types import typing import asyncio @types.coroutine def yield_option() -> typing.Generator: doing.start_preparing() while not doing.is_ready(): yield return doing.do() def future_and_call_soon_option() -> asyncio.Future: doing.start_preparing() loop = asyncio.get_running_loop() future = loop.create_future() def inner(): if not doing.is_ready(): loop.call_soon(inner) else: future.set_result(doing.do()) inner() return future
测试代码如下:
async def main(): await asyncio.gather(yield_option(), future_and_call_soon_option()) # 并发运行 asyncio.run(main())
两种方案都能正常运行,但哪种方案更优?是否存在更合适的第三种方案?
方案分析与优化建议
两种现有方案的优劣对比
1. yield_option 方案
- 优势:代码简洁直观,逻辑和原始阻塞函数高度对齐,容易理解。借助生成器协程的
yield关键字主动让出CPU,无需手动管理Future对象,适配asyncio事件循环的方式很直接。 - 劣势:属于忙等待轮询,每次
yield都会触发事件循环调度,在准备过程较长时会频繁占用CPU资源,产生不必要的性能开销。
2. future_and_call_soon_option 方案
- 优势:通过
loop.call_soon()递归调度,相比生成器方案减少了协程切换的额外开销,性能略优。 - 劣势:代码繁琐,需要手动创建、维护Future对象,递归调用的写法可读性差。本质还是忙等待轮询,同样存在CPU资源浪费的问题。
更优的第三种方案
针对上述方案的轮询痛点,有两种更合理的优化方向:
方向1:加入休眠的异步轮询
如果doing的准备过程不需要毫秒级的响应精度,可在轮询间隙加入短暂休眠,大幅降低CPU占用。代码符合现代async/await语法,可读性强:
async def async_sleep_option(): doing.start_preparing() while not doing.is_ready(): await asyncio.sleep(0.01) # 休眠10毫秒,可根据需求调整间隔 return doing.do()
这种方案通过asyncio.sleep()主动让出CPU,仅在指定时间后再检查状态,既保证了异步协作,又避免了无意义的忙等待。
方向2:基于回调的异步通知(最优)
如果doing的API支持回调机制(即准备完成时触发指定函数),可以完全抛弃轮询,实现真正的异步通知:
def callback_based_option() -> asyncio.Future: loop = asyncio.get_running_loop() future = loop.create_future() def on_ready(): future.set_result(doing.do()) # 假设doing.start_preparing支持传入回调参数,准备完成时调用on_ready doing.start_preparing(on_ready=on_ready) return future
这种方案几乎没有CPU开销,只有当准备完成时才会触发后续逻辑,是性能最优的实现方式,但依赖doing提供回调支持。
内容的提问来源于stack exchange,提问作者Alexey_Chebotarev
相关产品推荐
相关产品推荐

