sync_to_async为何让time.sleep呈现异步并发特性?
我知道asgiref.sync.sync_to_async用于在异步上下文运行同步代码,它没法将同步代码魔术般转为真正的异步代码。但实际测试时遇到意外情况:它似乎让time.sleep表现得像异步sleep。
我设计了两个任务:第一个任务循环3次,每次间隔1秒;第二个任务通过time.sleep()等待3秒。原本预期无论直接调用同步等待函数还是通过sync_to_async调用,结果都应一致,但实际输出完全不同,想知道原因。
测试代码
import asyncio import time from asgiref.sync import sync_to_async async def first_job(): for i in range(3): print(time.time(), "first start") await asyncio.sleep(1) print(time.time(), "first finish") def sync_wait(): print(time.time(), "wait start") time.sleep(3) print(time.time(), "wait finish") async def second_job_sync_wait(): print(time.time(), "second start") sync_wait() print(time.time(), "second finish") async def second_job_async_wait(): print(time.time(), "second start") await sync_to_async(sync_wait)() print(time.time(), "second finish") async def main(): print("========== sync wait:") await asyncio.gather(second_job_sync_wait(), first_job()) print("========== async wait:") await asyncio.gather(second_job_async_wait(), first_job()) asyncio.run(main())
输出结果
========== sync wait: 1676477101.0311968 second start 1676477101.031214 wait start 1676477104.036779 wait finish 1676477104.0369549 second finish 1676477104.037318 first start (0) 1676477105.0388129 first finish (0) 1676477105.03891 first start (1) 1676477106.040628 first finish (1) 1676477106.041095 first start (2) 1676477107.043103 first finish (2) ========== async wait: 1676477107.043814 second start 1676477107.045408 wait start 1676477107.045689 first start (0) 1676477108.047393 first finish (0) 1676477108.047503 first start (1) 1676477109.048685 first finish (1) 1676477109.048785 first start (2) 1676477110.04971 first finish (2) 1676477110.050215 wait finish 1676477110.0516758 second finish
原因解析
直接调用同步函数的场景
当second_job_sync_wait直接调用sync_wait时,time.sleep(3)会阻塞整个异步事件循环。因为同步代码是在当前异步任务的执行线程中直接运行的,事件循环被完全卡住,无法切换到first_job执行,必须等sync_wait的3秒睡眠结束后,first_job才会开始执行。这就是为什么输出中first start要等到3秒后才出现。
使用sync_to_async包装的场景
sync_to_async的核心逻辑是将同步函数放到独立的线程中执行,从而避免阻塞异步事件循环。当你await sync_to_async(sync_wait)()时,事件循环会暂停当前的second_job_async_wait任务,转而调度其他可执行的任务(也就是first_job),等线程中的sync_wait执行完毕(3秒后),再回到second_job_async_wait继续执行后续代码。
这种线程隔离的方式,让两个任务看起来像是“并行”执行,给人一种time.sleep变成异步操作的错觉,但本质上time.sleep还是同步的——只是它的阻塞被限制在单独的线程里,不会影响事件循环调度其他异步任务。
内容的提问来源于stack exchange,提问作者Saeed Mofidi

