flask是如何管理上下文的

1.什么是上下文

通俗的说,就是一个容器,存储了一些与程序运行相关的参数变量等,这些内容支持着程序的运行。

在flask中谈论的上下文为两个:请求上下文和应用上下文。比如常用的gsessionrequest,属于请求上下文,其内容只在各自的请求中有效。而current_app就是应用上下文。flask引入应用上下文的概念是为了更好的支持多应用开发。

2.flask是如何管理上下文的

2.1 继续从上篇文章falsk是如何处理请求的接着说。上篇文章说到wsgi_app时,提到了调用self.request_context方法时会创建请求上下文对象。

def wsgi_app(self, environ, start_response):
    # 创建当前请求的上下文空间
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            # 将上下文压入栈中
            ctx.push()
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        except:  # noqa: B001
            error = sys.exc_info()[1]
            raise
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        # 将当前上下文从栈空间弹出
        ctx.auto_pop(error)

2.2 那就先看一下self.request_context执行了什么(已删减部分源码)。内部首先是实例化了RequestContext这个类(这个名字很明显吧),实例化方法内部还实例化了request_class这个类(继承自BaseRequest),返回了请求对象,这个请求对象包含了请求的相关信息。RequestContext类中实现了两个重要的方法:push和pop方法。看到这里,依稀明白了请求上下文的处理流程。

上面wsgi_app中提到的full_dispatch_request方法在处理请求时,会到_request_ctx_stack取栈顶的请求上下文(可继续看源码,内容太多,就不贴出来了),对请求处理结束返回相应的响应对象后,再调用auto_pop(内部调用pop)将请求上下文从栈空间弹出。

flask是支持多线程和协程的,比如多线程访问时,flask是如何保证取请求上下文不会取到同一个呢?

def request_context(self, environ):
    # self即当前app,environ是请求的参数
    return RequestContext(self, environ)

class RequestContext(object):

    def __init__(self, app, environ, request=None, session=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self._implicit_app_ctx_stack = []

        self.preserved = False

        
    def push(self):
        """Binds the request context to the current context."""
         # 取_request_ctx_stack栈顶的请求上下文
        top = _request_ctx_stack.top
        # 如果某次异常,会导致上次请求上下文没有正常弹出,这里确保栈顶没有请求上下文
        if top is not None and top.preserved:
            top.pop(top._preserved_exc)
         # 取_app_ctx_stack栈顶的应用上下文
        app_ctx = _app_ctx_stack.top
        # 确保当前请求上下文在这个应用上下文内
        if app_ctx is None or app_ctx.app != self.app:
            app_ctx = self.app.app_context()
            app_ctx.push()
            self._implicit_app_ctx_stack.append(app_ctx)
        else:
            self._implicit_app_ctx_stack.append(None)

        if hasattr(sys, "exc_clear"):
            sys.exc_clear()
         # 将自身压入_request_ctx_stack栈中
        _request_ctx_stack.push(self)


    def pop(self, exc=_sentinel):
        app_ctx = self._implicit_app_ctx_stack.pop()

        try:
            clear_request = False
            if not self._implicit_app_ctx_stack:
                self.preserved = False
                self._preserved_exc = None
                if exc is _sentinel:
                    exc = sys.exc_info()[1]
                self.app.do_teardown_request(exc)

                if hasattr(sys, "exc_clear"):
                    sys.exc_clear()

                request_close = getattr(self.request, "close", None)
                if request_close is not None:
                    request_close()
                clear_request = True
        finally:
            # 弹出当前请求上下文
            rv = _request_ctx_stack.pop()

            if clear_request:
                rv.request.environ["werkzeug.request"] = None

            # Get rid of the app as well if necessary.
            if app_ctx is not None:
                app_ctx.pop(exc)
             # 确保弹出的上下文空间是自身
            assert rv is self, "Popped wrong request context. (%r instead of %r)" % (
                rv,
                self,
            )

2.3 答案是使用线程或协程的唯一标识,即get_ident这个函数。来看_request_ctx_stack源码,_request_ctx_stack是个全局变量,一开始实例化Flask类时,就会实例化LocalStack这个类并且导入了这个变量。

调用push方法时,会触发self._local的__getattr__方法,如果self._local没有存储当前线程或协程的唯一标识,会触发自身的__setattr__方法,然后将当前请求上下文存储到这个__storage__属性中,这样就保证并发请求时,正确使用对应的上下文啦。

_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()

try:
    from greenlet import getcurrent as get_ident
except ImportError:
    try:
        from thread import get_ident
    except ImportError:
        from _thread import get_ident

class Local(object):
    __slots__ = ("__storage__", "__ident_func__")

    def __init__(self):
        object.__setattr__(self, "__storage__", {})
        object.__setattr__(self, "__ident_func__", get_ident)

    def __getattr__(self, name):
        try:
            return self.__storage__[self.__ident_func__()][name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        ident = self.__ident_func__()
        storage = self.__storage__
        try:
            storage[ident][name] = value
        except KeyError:
            storage[ident] = {name: value}

            
class LocalStack(object):

    def __init__(self):
        self._local = Local()

    def push(self, obj):
        """Pushes a new item to the stack"""
        # 调用self._local对象中的__getattr__方法
        rv = getattr(self._local, "stack", None)
        if rv is None:
            # 调用self._local对象中的__setattr__方法,设置当前线程或协程唯一标识
            self._local.stack = rv = []
        # 压入栈中当前的请求上下文
        # 最终self._local中__storage__的内容类似为:
        # {241253254325: {'stack': RequestContext}}
        rv.append(obj)
        return rv

    def pop(self):
        pass

    @property
    def top(self):
        pass

SyntaxError
199 声望19 粉丝