bigywater.tech
Django · 113

sync_to_async 的线程行为

在 async 视图里调用同步代码要用 sync_to_async。它有一个 thread_sensitive 参数,决定「所有同步代码是不是共用一个线程」。这篇在真实的 uvicorn 里量出两种模式的线程与耗时差别。

True vs False
10 个 0.2 s 任务:2037 ms(1 线程)vs 206 ms(10 线程)
每请求独立
20 个并发请求 → 20 个线程,互不排队
14 个线程
50 个任务在 thread_sensitive=False 下只用到的线程数(本机)

thread_sensitive=True:同一个请求共用一个线程

实测 · 本机执行输出
   ['ThreadPoolExecutor-2_0', 'ThreadPoolExecutor-2_0', 'ThreadPoolExecutor-2_0', 'ThreadPoolExecutor-2_0', 'ThreadPoolExecutor-2_0'] → 不同线程数 1

为什么要这样?因为 Django 的数据库连接是「每线程一个」,并且很多同步代码(ORM、部分中间件、事务)假定自己一直在同一个线程里。所以默认让同一个请求里的同步代码都回到同一个线程里执行。(这一段解释来自设计上的理解,本文实验验证的是「线程相同」这一现象。)

代价:串行

实测 · 本机执行输出
  thread_sensitive=True  总耗时    2037 ms   用到的线程数 1
  thread_sensitive=False 总耗时     206 ms   用到的线程数 10

同一请求里 gather 多个 sync_to_async,默认模式下会排队执行。要并发就得 thread_sensitive=False,但这时每个任务在不同线程里,ORM 会在每个线程里开自己的数据库连接(下一篇 115 会在 PostgreSQL 上量到)。

不同请求不会互相排队

实测 · 本机执行输出
  20 个并发请求总耗时 518 ms,用到 20 个不同线程

线程池上限

实测 · 本机执行输出
  总耗时 819 ms,用到 14 个线程

50 个任务只用了 14 个线程,总耗时约 4 个批次。这个上限我没有去拆它是由什么决定的(默认线程池大小、ASGI_THREADS 等),只报告本机现象。

分步演示

sync_to_async 的线程
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。uvicorn 1 个进程,视图里用 sync_to_async 包 time.sleep
    • 没有验证:线程池上限由什么决定、ASGI_THREADS 环境变量的作用、多 worker、嵌套 async_to_sync / sync_to_async 的线程归属
    京ICP备2026067502号-1京公网安备11010502063487号