web框架——flask3.x-上下文管理机制

flask3.x——上下文管理机制


1. 上下文隔离

  • 核心目标:解决多请求并发时的数据混乱问题,让每个请求的相关数据

  • 互不干扰,为接收请求时做环境铺垫。

  • 实现工具:用contextvars模块里面的contextvar类 解决线程/协程数据隔离问题

  • 作用:确保不同请求的的requestcurrent_app等数据各用各的,而不是串用

  • Contextvar简概:是python中用于实现线程/协程级数据隔离的核心工具,能让不同执行单元(如请求)拥有专属数据,互不干扰

    • 自动绑定标识:会自动关联当前线程/协程的唯一标识(线程/协程ID)

    • 精准数据定位:存取数据时根据唯一标识精确找到该执行单元的专属数据,而不会串拿其他单元的数据

    • 直接存储当前状态:通过_cv_app(应用上下文)和_cv_request(请求上下文)两个独立的ContextVar,直接存储当前活跃的上下文对象,不维护历史上下文的栈结构。即同一时刻只需要一个活跃的AppContext和一个活跃的RequestContext

    • 简化的嵌套模型:对于临时请求,不再通过嵌套栈实现,而是使用ContextVarset()方法直接替换当前上下文

    • 自动恢复ContextVarreset()方法可以自动恢复到之前的状态,相当于内置了"栈"的单个层级切换

     

2. 上下文管理机制(新旧版本对比)

  • Flask版本差异上下文管理机制说明:

    • Flask 2.x及更早版本:都依赖Werkzeug里面的Local.py 。Local类和LocalStack类配合实现上下文的隔离和管理,通过LocalStack类维护栈结构,管理嵌套上下文。

    • Flask 3.x及更新版本:不再依赖Werkzeug里面的Local.py。直接使用Python内置的ContextVar存储上下文对象,通过令牌管理嵌套上下文。

  • 上下文管理机制核心目标:

    • 多个请求的隔离

    • 单个请求的内的多个Context有序管理

  • 上下文管理机制实现方式差异:

    • Flask 3.x及更新版本:

      • 使用Python内置的ContextVar实现

      • 直接管理当前上下文状态,不再使用栈结构

      • 分别存储AppContextRequestContext,通过令牌结合上下文类的push/pop方法管理

    • Flask 2.x及更早版本:

      • 使用LocalStack类实现栈结构管理

      • Context对象按顺序压入栈中管理

      • 通过栈的后进先出特性实现多个Context的有序切换

  • 上下文管理机制新旧版本总结

    • Flask 3.x及更新版本总结:它通过_cv_app(应用上下文)和_cv_request(请求上下文)两个独立的ContextVar实例,直接存储当前活跃的上下文对象。当出现临时上下文操作时,会执行set()方法将临时上下文设为当前活跃状态,覆盖原有上下文值。当临时请求处理完后,执行reset()方法恢复到之前的上下文状态。整个过程结合try-finally块,确保无论是否发生异常,上下文都能正确恢复,避免上下文混乱。。

    • Flask 2.x及更早版本总结:通过_app_ctx_stack(应用上下文栈)和_request_ctx_stack(请求上下文栈)两个独立的LocalStack实例,以栈结构管理上下文对象。当出现临时上下文操作时,会执行栈的push()方法将临时上下文压入栈顶,使其成为当前活跃上下文,而当这个临时请求处理完后,执行栈的pop()方法移除栈顶的临时上下文,恢复到之前的上下文状态。整个过程结合try-finally块,确保无论是否发生异常,上下文都能正确出栈,避免栈结构混乱。

 

3. 上下文到底是什么及上下文管理机制的实现

  • 上下文:

    • 简而言之一篇文章甚至一部小说,试卷出题人经常会让你进行联系上下文分析作者当前写这句话的心情,此时就得联系上下文即根据文本中出现的信息来进行有逻辑有思维有步骤的分析然后进行作答。

    • 此过程中你肯定得记住这些信息,对应到Flask的上下文中其也就是用于存储信息然后进行程序的有内容有顺序的执行。然而要存储这些信息就有了请求上下文RequestContext和应用上下文Appcontext,而之后要进行程序有内容有顺序的执行就得需要上下文管理机制。

  • 上下文管理机制的实现:

    • 如果没有这个机制会怎么样:

      • 没有上下文管理机制ContextVar或者LocalStack,单个请求内的多段逻辑会因临时数据污染、上下文状态混乱而报错。

      • 比如现在有HTTP请求过来:首先创建请求上下文RequestContext再在其 push() 方法中创建/激活应用上下文AppContext,若后面有个临时上下文操作(比如代码中调用某个工具函数来完成某个功能),此时会新建临时 AppContext ,不会新增 RequestContextRequestContext 与当前用户请求强绑定,全程唯一,包含 request 、 session 等请求数据):

        • 若没有合适的上下文管理机制

          • Flask 2.x及更早版本:

            1. 依赖_app_ctx_stackAppContext 栈)和 _request_ctx_stackRequestContext 栈)管理上下文 。

            2. 没有LocalStack 将 AppContext 维护成栈,就无法区分主AppContext 和临时 AppContext ,销毁时可能误删主 AppContext ,导致主请求的后续操作无法正常进行。

          • Flask 3.x及更新版本:

            1. 依赖_cv_app (存储 AppContext)和 _cv_request (存储 RequestContext )两个 ContextVar管理上下文

            2. 没有 ContextVar 的Token状态记录时,临时操作后无法恢复主上下文状态,导致上下文混乱。

        • 视化

          • Flask 2.x及更早版本:_app_ctx_stack : [主AppContext, 临时AppContext],_request_ctx_stack : [主RequestContext] (始终唯一)

          • Flask 3.x及更新版本:通过 ContextVar 分别存储 ppContext RequestContext ,用Token记录 AppContext 的嵌套状态,实现与栈等价的上下文切换, RequestContext 保持唯一

 

4. 上下文管理机制源码分析

  • Flask 3.x及更新版本核心实现组件源码拆解

    • 上下文存储方式

      • 使用两个独立的ContextVar实例作为上下文存储容器

    # flask/globals.py
    ​
    _cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx")
    """
    _cv_app= ContextVar("flask.app_ctx")
    表示创建了一个名为_cv_app的Contextvar变量
    "flask.app_ctx"
    表示这个Contextvar数据隔离工具的标识名称为"flask.app_ctx"
    _cv_app:ContextVar[AppContext]
    表示这个变量的类型为Contextvar且这个变量只能存储AppContext类型的对象
    """
    ​
    _cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx")
    """
    _cv_request= ContextVar("flask.request_ctx")
    表示创建了一个名为_cv_request的Contextvar变量
    "flask.request_ctx"
    表示这个Contextvar数据隔离工具的标识名称为"flask.app_ctx"
    _cv_request: ContextVar[RequestContext]
    表示这个变量的类型为Contextvar且这个变量只能存储RequestContext类型的对象
    """

    image

     

    • 上下文代理方式

      • 通过LocalProxy创建了一系列全局可访问的代理对象,简化上下文访问

    app_ctx: AppContext = LocalProxy(  # type: ignore[assignment]
        _cv_app, unbound_message=_no_app_msg
    )
    current_app: Flask = LocalProxy(  # type: ignore[assignment]
        _cv_app, "app", unbound_message=_no_app_msg
    )
    g: _AppCtxGlobals = LocalProxy(  # type: ignore[assignment]
        _cv_app, "g", unbound_message=_no_app_msg
    )
    ​
    ​
    ​
    request_ctx: RequestContext = LocalProxy(  # type: ignore[assignment]
        _cv_request, unbound_message=_no_req_msg
    )
    request: Request = LocalProxy(  # type: ignore[assignment]
        _cv_request, "request", unbound_message=_no_req_msg
    )
    session: SessionMixin = LocalProxy(  # type: ignore[assignment]
        _cv_request, "session", unbound_message=_no_req_msg
    )

     

    • 上下文类实现

      • AppContext

        • 应用上下文,用于存储应用级别的信息

      class AppContext:
      ​
          def __init__(self, app: Flask) -> None:
              """
              将Flask实例保存到self.app中,便于以后应用上下文在其他地方可以便于访问应用实例
              创建URL适配器,调用了app中create_url_adapter()方法,便于将请求的URL路径匹配到对应的视图函数
              创建单次请求生命周期中的全局对象g,便于用于存储单次请求生命周期中重要且反复需要的数据
              创建一个空列表,用于存储ContextVar操作的令牌,便于pop操作时ContextVar利用令牌恢复到之前的应用上下文状态
              """
              self.app = app
              self.url_adapter = app.create_url_adapter(None)
              self.g: _AppCtxGlobals = app.app_ctx_globals_class()
              self._cv_tokens: list[contextvars.Token[AppContext]] = []
      ​
          def push(self) -> None:
              #绑定应用上下文到当前上下文
              """
              _cv_app.set(self):将当前(self)应用上下文设置为_cv_app:ContextVar的值,即表示其为当前活跃的上下文。且会                          返回一个Token(令牌)用于恢复之前上下文的状态信息
              self._cv_tokens.append():将返回的Token添加到_cv_tokens列表中
              appcontext_pushed.send():发送应用上下文推入信号
               """
              self._cv_tokens.append(_cv_app.set(self))
              appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync)
      ​
          def pop(self, exc: BaseException | None = _sentinel) -> None:  # type: ignore
              #弹出应用上下文
              """
              pop时如果是最后一个令牌
              获取当前异常(如果有)
              调用应用的 do_teardown_appcontext() 方法,执行所有注册的应用上下文清理函数
              """
              try:
                  if len(self._cv_tokens) == 1:
                      if exc is _sentinel:
                          exc = sys.exc_info()[1]
                      self.app.do_teardown_appcontext(exc)
                      
              """
              不论是否为最后一个上下文均执行finally块,确保上下文正确切换
              ctx = _cv_app.get():从ContextVar中获取当前活跃的应用上下文
              _cv_app.reset(self._cv_tokens.pop()):将Token列表中的令牌进行获取,进行恢复之前应用上下文的状态
              """
              finally:
                  ctx = _cv_app.get()
                  _cv_app.reset(self._cv_tokens.pop())
      ​
              
              """
              最后检查,finally块中获取的当前活跃的应用上下文是否为当前的应用上下文。如果不是则抛异常,确保上下文的正确切换
              """
              if ctx is not self:
                  raise AssertionError(
                      f"Popped wrong app context. ({ctx!r} instead of {self!r})"
                  )
                  
              """
              发送当前应用上下文弹出信号
              """
              appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync)
              
      ​
              
              核心工作流程总结
      初始化:创建 AppContext 实例,存储应用实例、URL 适配器和全局对象 g
      推入上下文:
      调用 push() 方法
      将当前实例设置为全局 _cv_app ContextVar 的值
      存储返回的令牌
      发送应用上下文推入信号
      弹出上下文:
      调用 pop() 方法
      如果是最后一个令牌,执行应用上下文清理
      使用存储的令牌恢复 ContextVar 之前的状态
      验证弹出的上下文是否正确

       

      • RequestContext

        • 请求上下文,用于存储请求级别的信息

      class RequestContext:
      ​
          def __init__(
              self,
              app: Flask,
              environ: WSGIEnvironment,
              request: Request | None = None,
              session: SessionMixin | None = None,
          ) -> None:
              """
              初始化,存储应用实例、request、路由适配器、flash、session等
              """
              self.app = app
              self.request = request or app.request_class(environ)  # 请求对象
              self.url_adapter = None
              self.flashes = None
              self.session = session  # 会话对象
              self._after_request_functions = []  # 后处理函数列表
              self._cv_tokens: list[tuple[contextvars.Token[RequestContext], AppContext | None]] = []  # 令牌列表
      ​
      ​
          def push(self) -> None:
              #绑定请求上下文到当前上下文
              """
              应用上下文管理逻辑
              1.检查确保有活跃应用上下文存在
              2.如果,不存在或者应用上下文的应用实例和当前请求上下文的应用实例不一样则创建与当前请求上下文相符合的应用上下文
              app_ctx = self.app.app_context():使用应用实例的app_context()方法返回AppContext对象
              app_ctx.push():执行AppContext类的push方法将当前AppContext实例设置为全局 _cv_app ContextVar 的值
              3.否则,不在创建新的应用上下文
              4.最后存储请求上下文令牌,和应用上下文以元组形式将其归于一组到列表,用于管理嵌套上下文和状态恢复
              """
              app_ctx = _cv_app.get(None)
              if app_ctx is None or app_ctx.app is not self.app:
                  app_ctx = self.app.app_context()
                  app_ctx.push()
              else:
                  app_ctx = None
              self._cv_tokens.append((_cv_request.set(self), app_ctx))
      ​
              
              """
              会话初始化逻辑
              如果会话对象不存在
              1.获取应用的会话接口 app.session_interface
              2.调用open_session(self.app, self.request)尝试从请求中恢复会话
              3.如果恢复失败 if self.session is None ,创建一个新的会话 make_null_session
              """
              if self.session is None:
                  session_interface = self.app.session_interface
                  self.session = session_interface.open_session(self.app, self.request)
      ​
                  if self.session is None:
                      self.session = session_interface.make_null_session(self.app)
      ​
                      
              if self.url_adapter is not None:
                  self.match_request()
      ​
          def pop(self, exc: BaseException | None = _sentinel) -> None:  # type: ignore
              """
              判断是否为最后一个请求上下文
              clear_request = len(self._cv_tokens)
              判断:clear_request == 1
              """
              clear_request = len(self._cv_tokens) == 1
      ​
              
              """
              pop时如果是最后一个请求上下文
              获取当前异常(如果有)
              调用do_teardown_request执行请求上下文清理函数
              检查请求对象是否有close方法,如果有则调用关闭资源
              """
              try:
                  if clear_request:
                      if exc is _sentinel:
                          exc = sys.exc_info()[1]
                      self.app.do_teardown_request(exc)
      ​
                      request_close = getattr(self.request, "close", None)
                      if request_close is not None:
                          request_close()
                          
                          
              """
              无论是否有异常均执行finally块
              1.获取当前请求上下文,并获取令牌便于恢复之前的请求上下文状态,和获取与之相关的应用上下文
              2.利用令牌恢复之前的请求上下文状态
              3.如果是最后一个请求上下文,将请求对象从WSGI环境中移除
              4.如果app_ctx不为None(表示替换这个请求上下文时,创建过与之相配合的应用上下文),则清除这个应用上下文
              5.确保弹出的请求上下文是当前实例,如果不是则抛异常
              
              """
              finally:
                  ctx = _cv_request.get()
                  token, app_ctx = self._cv_tokens.pop()
                  _cv_request.reset(token)
      ​
                  if clear_request:
                      ctx.request.environ["werkzeug.request"] = None
      ​
                  if app_ctx is not None:
                      app_ctx.pop(exc)
      ​
                  if ctx is not self:
                      raise AssertionError(
                          f"Popped wrong request context. ({ctx!r} instead of {self!r})"
                      )
                      
               
                      总结
      RequestContext类实现了Flask的请求上下文管理,核心功能包括:
      ​
      初始化请求相关的资源(请求对象、会话、URL适配器等)
      在push时确保应用上下文存在并正确管理嵌套上下文
      在pop时按正确顺序恢复上下文状态并清理资源
      通过令牌机制支持嵌套上下文的正确管理
      确保资源的正确释放和异常安全

image

image

 

 

 

 

posted @ 2025-12-08 21:10  guohan  阅读(0)  评论(0)    收藏  举报