bigywater.tech
Flask · 9

before_first_request 的变化与替代方案

很多旧教程里的 @app.before_first_request 在新版本里报错。要实现「第一次请求前做一次」应该怎么写?并发和多进程下会不会重复执行?这篇用真实的 gunicorn 并发请求一一数出来。

不存在
Flask 3.1.3 里 before_first_request 的状态
12 → 1
12 个并发首批请求下 init 执行次数:不加锁 → 加锁
3 → 1
3 个 worker 下模块级 init 执行次数:默认 → --preload

before_first_request 没了

实测 · 本机执行输出
  Flask 版本 3.1.3 | hasattr(app, "before_first_request") = False
  用 @app.before_first_request → AttributeError: 'Flask' object has no attribute 'before_first_request'
  Flask 类上还留着的 before_ 开头的方法: ['before_request']

上一篇:before_request、after_request、teardown_request;下一篇:异常处理器的匹配顺序。

应用工厂里初始化

实测 · 本机执行输出
  源码:
      1 | def create_app():
      2 |     app = Flask(__name__)
      3 |     with app.app_context():
      4 |         warm_up()          # 加载配置、建表、预热缓存……
      5 |     return app
  create_app() 耗时 61 ms(包含 50 ms 的 warm_up);之后 5 个请求,warm_up 总共执行了 1 次

before_request 加标志位

实测 · 本机执行输出
  app.py 中两种写法(省略 init 的定义):
      1 | def init(tag):
      2 |     time.sleep(0.2)
      3 |     with open(LOG, "a") as f: f.write(f"{tag} pid={os.getpid()}\n")
      4 | if os.environ["MODE"] == "nolock":
      5 |     @app.before_request
      6 |     def first():
      7 |         global _done
      8 |         if not _done:
      9 |             init("nolock"); _done = True
     10 | elif os.environ["MODE"] == "lock":
     11 |     @app.before_request
     12 |     def first():
     13 |         global _done
     14 |         if not _done:
     15 |             with _lock:
     16 |                 if not _done:
     17 |                     init("lock"); _done = True
     18 | elif os.environ["MODE"] == "module":
     19 |     init("module-level")
  MODE=nolock  1 个 worker、16 线程,12 个并发的第一批请求:init 执行了 12 次(都返回 200: True)
  MODE=lock    1 个 worker、16 线程,12 个并发的第一批请求:init 执行了 1 次(都返回 200: True)
  MODE=module  1 个 worker、16 线程,12 个并发的第一批请求:init 执行了 1 次(都返回 200: True)

多个 worker 进程

实测 · 本机执行输出
  --workers 3              MODE=module:init 执行 3 次,涉及 3 个不同的 pid
  --workers 3 --preload    MODE=module:init 执行 1 次,涉及 1 个不同的 pid
  --workers 3 MODE=lock:   init 执行 3 次,涉及 3 个不同的 pid(线程锁只在进程内有效)

分步演示

第一次请求前的初始化
本步的对象

    本文用到的工具与边界

    • 本机 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 --threads 16 / --workers 3
    • 没有验证:uWSGI 的 lazy-apps 与预加载差别(见第 173 篇)、Flask 2.2 之前 before_first_request 的旧行为(本机只有 3.1.3)、flask run 开发服务器下的重载器对初始化次数的影响
    • 「方案 B 不加锁会重复执行」是在 init 里放了 0.2 秒的 sleep 来放大竞态后测得的次数,真实应用里的次数取决于初始化耗时与并发量
    京ICP备2026067502号-1京公网安备11010502063487号