bigywater.tech
Flask · 6

Context Local 与线程隔离

Flask 靠什么保证两个同时进来的请求看到的是各自的 request 和 g?线程、协程、线程池分别怎么样?这篇用 ContextVar、threading.local 和真实的 gunicorn 并发请求逐个验证。

0
16 个并发请求里 g 值读回不一致的个数
3 / 3
3 个协程用 ContextVar 各读到自己的值(threading.local 则全读到 Z)
0
sys.flags.thread_inherit_context(新线程默认不继承上下文)

线程里的 ContextVar

实测 · 本机执行输出
  新线程里 cv.get(): 默认值 | 把 copy_context() 带进线程用 ctx.run: 主线程设的值
  Python 版本 3.14.7 | sys.flags.thread_inherit_context = 0

上一篇:current_app、request、g 的实现;下一篇:请求进入 Flask 后的完整生命周期。

asyncio 任务

实测 · 本机执行输出
  任务A 睡醒后读到 任务A 设的值
  任务B 睡醒后读到 任务B 设的值
  任务C 睡醒后读到 任务C 设的值
  gather 结束后 main 里 cv.get() = main 设的值

threading.local 的问题

实测 · 本机执行输出
  threading.local: ['X 睡醒后读到 Z', 'Y 睡醒后读到 Z', 'Z 睡醒后读到 Z']
  ContextVar     : ['X 睡醒后读到 X', 'Y 睡醒后读到 Y', 'Z 睡醒后读到 Z']

werkzeug.local.Local

实测 · 本机执行输出
  3 个线程各自读到: ['线程0', '线程1', '线程2'] | 3 个协程各自读到: ['任务0', '任务1', '任务2'] | 主线程读到: 主线程
  Local 的内部存储: ContextVar (ContextVar)
  release_local(L) 之后访问 L.name: AttributeError: name

gunicorn 多线程下的 g

实测 · 本机执行输出
  16 个并发请求(单 worker、8 线程)总耗时 481 ms;g 里读回来的值与自己的参数不一致的请求数:0;示例:['0|0|thread-ok', '1|1|thread-ok', '2|2|thread-ok']

线程池

实测 · 本机执行输出
  直接 submit: RuntimeError
  copy_current_request_context: /
  copy_context().run: /

分步演示

上下文隔离
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.7 / Flask 3.1.3 / Werkzeug 3.1.9 / Jinja2 3.1.6 / SQLAlchemy 2.1.1 / gunicorn 26.2.0(都装在自己的虚拟环境里);大多数实验用 Flask 的 test_client(直接调用 WSGI 应用,不经过网络),需要真实 HTTP 时另起 gunicorn 或开发服务器(端口 9600 以上,本机 127.0.0.1);计时用 time.perf_counter,数字每次运行会有小幅抖动。gunicorn 用 --workers 1 --threads 8
    • 没有验证:gevent / eventlet 下的 greenlet 隔离(见第 142 篇)、free-threaded(无 GIL)Python 构建下的行为、thread_inherit_context 设为 1 时的表现、更大并发下的 g 隔离
    京ICP备2026067502号-1京公网安备11010502063487号