FastAPI中同步代码为何不会阻塞事件循环?
嗨,这个问题问得非常精准,刚好戳中了Python异步编程和FastAPI底层机制的核心细节!我来一步步给你拆解清楚:
首先直接点出你看到的"并行"现象的核心原因:FastAPI(底层依赖Starlette/AnyIO)会把同步路由的执行完全放到独立的后台线程池里,和运行事件循环的主线程彻底分离,这就保证了同步任务不会阻塞事件循环处理其他请求。
1. FastAPI对同步路由的处理逻辑
当你用def定义路由时,FastAPI绝不会让事件循环直接执行这个同步函数,而是会把它提交给一个后台线程池(默认由AnyIO管理,线程数默认是min(32, os.cpu_count() + 4))。
简单说就是:
- 主线程:专门运行asyncio事件循环,负责接收请求、匹配路由,以及执行异步路由的代码
- 同步路由的代码:在独立的后台线程中运行,和主线程的事件循环互不干扰(或者说,两者的资源抢占由GIL规则控制)
这就意味着,当同步任务在后台线程跑的时候,主线程的事件循环是完全空闲的,可以正常处理其他请求——比如你发起的/async路由请求。
2. 关于GIL的关键澄清
你提到的"Python一次只能执行一个线程"是对GIL的常见误解,准确的说法是:在CPython中,同一时间只有一个Python线程能执行字节码,但有两种明确的场景会触发GIL的释放,让其他线程获得执行机会:
- 线程遇到I/O阻塞操作(比如文件读写、网络请求)时,会主动释放GIL
- 线程连续执行了一定数量的字节码(默认是1000条指令)后,会主动释放GIL
回到你的测试例子:
/sync路由的代码是CPU密集型循环,它并不会一直霸占GIL——每执行1000条字节码,就会释放一次GIL,这时候主线程的事件循环就有机会运行,处理/async路由的await asyncio.sleep(5)。- 而
/async路由里的await asyncio.sleep(5)是主动把任务挂起,告诉事件循环"我要暂停5秒,这段时间你去处理别的任务",所以事件循环完全不用等这个sleep完成,就能继续响应其他逻辑。
宏观上看,后台线程的同步循环和主线程的异步sleep就像是"并行"执行的,这就是你看到的结果。
3. 你的测试代码小细节
顺便提一句:你的异步路由函数名写错啦,应该是tarefa_assincrona(和同步路由重名了),不过这不会影响测试结果,只是容易混淆~
如果你的同步循环执行时间刚好和异步sleep的5秒差不多,两者就会几乎同时完成;如果同步循环的执行时间远超过5秒,你会看到异步任务先完成,同步任务继续运行,这也完全符合我们刚才的机制解释。
4. 极端场景的补充说明
如果你的同步任务是极端CPU密集型(比如无限循环的计算),它会不会把GIL一直霸占住,导致事件循环阻塞?其实也不会——因为事件循环线程在处理异步任务时,大多是处于等待I/O(比如网络请求、sleep)的状态,这些场景下线程不需要持有GIL,所以同步线程的GIL持有不会影响事件循环处理这些等待中的异步任务。
总结一下:
FastAPI通过线程池实现了同步任务和事件循环的解耦,再结合CPython GIL的释放规则,最终让同步路由和异步路由能够宏观上并行处理,这就是你观察到的现象背后的完整逻辑。
备注:内容来源于stack exchange,提问作者João Pedro Zimmermann

