(十二)把已有阻塞调用接到异步程序
1. 放进异步函数后仍会阻塞
# 项目文件:demos/blocking_loop.py
# 错误示例:time.sleep 在事件循环所在的线程中睡眠,心跳要等它结束才能推进。
import asyncio
import time
async def blocking(trace):
trace.append("阻塞开始")
time.sleep(0.02)
trace.append("阻塞结束")
async def heartbeat(trace):
await asyncio.sleep(0)
trace.append("心跳")
async def main():
trace = []
await asyncio.gather(blocking(trace), heartbeat(trace))
print(trace)
if __name__ == "__main__":
asyncio.run(main())输出 ['阻塞开始', '阻塞结束', '心跳']。网络库如果只有阻塞接口,直接在 async def 中调用,也会在相应等待段阻塞循环。
2. 用 to_thread 等待已有同步函数
# 项目文件:demos/to_thread_bridge.py
# 同步函数在线程中等待,事件循环可以继续运行并最终发出释放信号。
import asyncio
from threading import Event
def legacy_read(release):
release.wait()
return "同步读取完成"
async def main():
release = Event()
task = asyncio.create_task(asyncio.to_thread(legacy_read, release))
try:
await asyncio.sleep(0)
print("事件循环继续运行")
finally:
release.set()
print(await task)
if __name__ == "__main__":
asyncio.run(main())输出 事件循环继续运行 和 同步读取完成。to_thread(函数, 实参...) 把普通函数放到线程中执行,交回一个可等待操作;需要并发时再调度为任务。它适合衔接现有阻塞 I/O,不会把普通计算自动变成多核计算。
取消外层等待不会强行杀死线程,所以同步网络调用仍要有自己的连接、读取超时,长期工作还需要自己的结束信号。网络库已经提供异步接口时,可以直接交给事件循环,不必为每个请求再包一层线程。异步开发中的阻塞调用