前言
上一篇中我们已经知道flask运行的大体流程(Flask的工作原理),其中进入wsgi_app中首先创建的就是上下文环境,那么什么是上下文呢,又有什么作用。在了解上下文之前,先要弄清楚LocalProxy,Local,LocalStack这三个概念。
Local
根据werkzeug文档介绍,local是提供了线程隔离的数据访问方式,类似于python中的 thread locals。可以理解为存在一个全局的数据,不同线程可以读取和修改它。不同线程之间是彼此隔离的。
什么是线程隔离呢?
比如存在一个全局变量数字10,在线程1中把10改为1,主线程中读取这个数字,发现数字变成了1,也就是说新线程数据影响了主线程数据。这样一来,多线程之间考虑其他线程带来的影响,从而不能安全地读取和修改数据。
import threading
class A:
a = 10
obj = A()
def worker1():
"""线程1"""
obj.a = 1
t1 = threading.Thread(target=worker1, name='线程1')
t1.start()
t1.join()
print(obj.a)
结果
1
为什么不使用python thread local呢?因为这个有一些缺陷,比如
- 有些应用使用的是greenlet协程,这种情况下无法保证协程之间数据的隔离,因为不同的协程可以在同一个线程当中。
- 即使使用的是线程,WSGI应用也无法保证每个http请求使用的都是不同的线程,因为后一个http请求可能使用的是之前的http请求的线程,这样的话存储于thread local中的数据可能是之前残留的数据。
而Local解决了上面的问题,实现了线程之间的数据隔离,从而能够安全的读取和修改数据。
werkzeug中Local的实现
class Local(object):
__slots__ = ("__storage__", "__ident_func__")
def __init__(self):
object.__setattr__(self, "__storage__", {})
object.__setattr__(self, "__ident_func__", get_ident)
def __iter__(self):
return iter(self.__storage__.items())
def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)
def __release_local__(self):
self.__storage__.pop(self.__ident_func__(), None)
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}
def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
local的实现方式是使用python中的dict结构,根据每个线程的id去创建独立的数据,当访问数据时,根据当前线程的id进行访问,每个线程访问自己的数据即可。这样就实现了线程隔离的数据访问。
Local引入了get_ident方法用于获取当前线程的id,将get_ident方法存储至类的__ident_func__属性中,将所有线程的数据存储至__storage__属性中,此属性对应的是一个二维dict,每个线程使用一个dict存储数据,而每个线程的dict数据作为__storage__的线程id对应的值。因此线程访问数据事实上是访问__storage__[ident][name],其中前面的ident为线程的id,后面name才是用户指定的数据key。而ident是Local自动获取的,用户可以透明进行线程隔离的数据访问与存储。
Local使用
我们可以单独使用Local,实现线程隔离。
import threading
from werkzeug.local import Local
obj = Local()
obj.a = 10
def worker1():
"""线程1"""
obj.a = 1
print('线程1中的a的值为:{}'.format(obj.a))
t1 = threading.Thread(target=worker1, name='线程1')
t1.start()
t1.join()
print('主线程中的a的值为:{}'.format(obj.a))
结果
线程1中的a的值为:1
主线程中的a的值为:10
obj是一个线程隔离的对象,所以线程1的改变没有影响到主线程。
LocalStack
LocalStack是一个多线程隔离栈结构,通过源码发现是基于Local实现的,提供了栈结构push,pop,top方法。
class LocalStack(object):
def __init__(self):
self._local = Local()
def __release_local__(self):
self._local.__release_local__()
# 获取线程对应的id,直接复用Local的__ident_func__,即使用get_ident获取线程id
@property
def __ident_func__(self):
return self._local.__ident_func__
@__ident_func__.setter
def __ident_func__(self, value):
object.__setattr__(self._local, "__ident_func__", value)
def __call__(self):
def _lookup():
rv = self.top
if rv is None:
raise RuntimeError("object unbound")
return rv
return LocalProxy(_lookup)
# 入栈
def push(self, obj):
"""Pushes a new item to the stack"""
rv = getattr(self._local, "stack", None)
if rv is None:
self._local.stack = rv = []
rv.append(obj)
return rv
# 出栈
def pop(self):
"""Removes the topmost item from the stack, will return the
old value or `None` if the stack was already empty.
"""
stack = getattr(self._local, "stack", None)
if stack is None:
return None
elif len(stack) == 1:
release_local(self._local)
return stack[-1]
else:
return stack.pop()
# 弹出栈顶元素
@property
def top(self):
"""The topmost item on the stack. If the stack is empty,
`None` is returned.
"""
try:
return self._local.stack[-1]
except (AttributeError, IndexError):
return None
LocalStack使用
LockStack也可以单独使用,可以理解为就是普通的栈结构,只是这个栈结构是数据安全的。
import threading
from werkzeug.local import LocalStack
ls = LocalStack()
ls.push(1)
def worker1():
"""线程1"""
print('线程1中的栈顶的值为:{}'.format(ls.top))
ls.push(2)
print('线程1中的栈顶的值为:{}'.format(ls.top))
t1 = threading.Thread(target=worker1, name='线程1')
t1.start()
t1.join()
print('主线程中的栈顶的值为:{}'.format(ls.top))
结果
线程1中的栈顶的值为:None
线程1中的栈顶的值为:2
主线程中的栈顶的值为:1
LocalProxy
LocalProxy是用来代理Local和LocalStack对象的。
class LocalProxy(object):
__slots__ = ("__local", "__dict__", "__name__", "__wrapped__")
def __init__(self, local, name=None):
object.__setattr__(self, "_LocalProxy__local", local)
object.__setattr__(self, "__name__", name)
if callable(local) and not hasattr(local, "__release_local__"):
# "local" is a callable that is not an instance of Local or
# LocalManager: mark it as a wrapped function.
object.__setattr__(self, "__wrapped__", local)
def _get_current_object(self):
"""Return the current object. This is useful if you want the real
object behind the proxy at a time for performance reasons or because
you want to pass the object into a different context.
"""
if not hasattr(self.__local, "__release_local__"):
return self.__local()
try:
return getattr(self.__local, self.__name__)
except AttributeError:
raise RuntimeError("no object bound to %s" % self.__name__)
可以看到LocalProxy通过是_get_current_object来获取代理的对象,然后执行相应的操作即可。对proxy执行的任意操作,都是直接通过被代理对象执行的。为了保证代理对象可以直接进行操作,LocalProxy重载了所有的基本方法,这样就可以随意对proxy对象执行操作。
那么为什么需要LocalProxy来代理Local或LocalStack对象呢?
LocalProxy的作用
下面看一个例子,直接使用LocalStack。
from werkzeug.local import LocalStack
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})
def get_user():
return user_stack.pop()
# 直接调用函数获取user对象
user = get_user()
print(user['name'])
print(user['name'])
结果
John
John
使用LocalProxy
from werkzeug.local import LocalStack, LocalProxy
user_stack = LocalStack()
user_stack.push({'name': 'Bob'})
user_stack.push({'name': 'John'})
def get_user():
return user_stack.pop()
# 使用 LocalProxy
user = LocalProxy(get_user)
print(user['name'])
print(user['name'])
结果
John
Bob
结果显而易见,直接使用LocalStack对象,user一旦赋值就无法再动态更新了,而使用Proxy,每次调用操作符,都会重新获取user,从而实现了动态更新user的效果。
而在_get_current_object方法中可以看到,对于可执行对象或方法,就是直接执行获取可执行对象或方法对应的返回值。而对于Local对象,则是获取name作为属性的值。但是要注意的是,所有的获取都是在执行操作的时候获取的,这样就可以随着程序运行动态更新。同样也可以解释了上面的例子中,为方法get_user创建的LocalProxy类型的proxy_user,可以两次执行proxy_user[‘name’]获取到不同值了,因为两次执行时,都会通过_get_current_object执行get_user方法,两次执行的结果不同,返回的值也就不同了。
了解了Local,LocalProxy,LocalStack,接下来看一下上下文。
什么是上下文
上下文多用于文章中,代表的一个整体环境,比如一篇文章,我们可以说下文中,访问到下文所陈述的内容,也可以说上文中,访问到上文中的内容,而我们这篇文章中每一段文字所代表的意思,都是要根据我们的上下文来决定的,因为你随便拿出来一句话不去结合整体的语境去理解出来的意思肯定不是准确的,所以,我们这篇文章的上下文就是我们整篇的中心思想。
这是文章中的上下文,那么程序中的上下文是什么?
程序中的上下文代表了程序当下所运行的环境,存储了一些程序运行的信息。比如在程序中我们所写的函数大都不是单独完整的,在使用一个函数完成自身功能的时候,很可能需要同其他的部分进行交互,需要其他外部环境变量的支持,上下文就是给外部环境的变量赋值,使函数能正确运行。
Flask中的上下文
flask中有两个上下文,请求上下文和应用上下文。
请求上下文
在 flask 中,可以直接在视图函数中使用 request 这个对象进行获取相关数据,而 request 就是请求上下文的对象,保存了当前本次请求的相关数据。请求上下文对象有:request、session。
- request
封装了HTTP请求的内容,针对的是http请求。举例:user = request.args.get('user'),获取的是get请求的参数。 - session
用来记录请求会话中的信息,针对的是用户信息。举例:session['name'] = user.id,可以记录用户信息。还可以通过session.get('name')获取用户信息。
应用上下文
flask 应用程序运行过程中,保存的一些配置信息,比如程序名、数据库连接、应用信息等。应用上下文对象有:current_app,g
- current_app
应用程序上下文,用于存储应用程序中的变量,可以通过current_app.name打印当前app的名称,也可以在current_app中存储一些变量,例如:
应用的启动脚本是哪个文件,启动时指定了哪些参数
加载了哪些配置文件,导入了哪些配置
连了哪个数据库
有哪些public的工具类、常量
应用跑再哪个机器上,IP多少,内存多大 - g变量
g 作为 flask 程序全局的一个临时变量,充当者中间媒介的作用,我们可以通过它传递一些数据,g 保存的是当前请求的全局变量,不同的请求会有不同的全局变量。
current_app 的生命周期最长,只要当前程序实例还在运行,都不会失效。
request 和 g 的生命周期为一次请求期间,当请求处理完成后,生命周期也就完结了。
curren_app,g,request,session都是线程隔离的,我们可通过源码发现。
上下文的定义
Flask上下文定义在globals.py上
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return getattr(top, name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, "request"))
session = LocalProxy(partial(_lookup_req_object, "session"))
g = LocalProxy(partial(_lookup_app_object, "g"))
这里定义了两个LocalStack栈,_request_ctx_stack是请求上下文栈,_app_ctx_stack是应用上下文栈,curren_app,g,request,session都是LocalStack栈顶元素。
上下文处理流程
在上一篇中(Flask的工作原理)我们看到wsgi_app中会创建上下文环境,调用 ctx.push() 函数将上下文信息压栈。
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)
ctx = self.request_context(environ)实际上是实例化一个RequestContext对象。
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.url_adapter = None
try:
self.url_adapter = app.create_url_adapter(self.request)
except HTTPException as e:
self.request.routing_exception = e
self.flashes = None
self.session = session
def push(self):
top = _request_ctx_stack.top
if top is not None and top.preserved:
top.pop(top._preserved_exc)
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.push(self)
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=_sentinel):
def auto_pop(self, exc):
def __enter__(self):
self.push()
return self
def __exit__(self, exc_type, exc_value, tb):
self.auto_pop(exc_value)
if BROKEN_PYPY_CTXMGR_EXIT and exc_type is not None:
reraise(exc_type, exc_value, tb)
ctx.push()是创建上下文环境,把该请求的 ApplicationContext 和 RequestContext 有关的信息保存到对应的栈上。
到了这里Flask上下文基本明确了,每次有请求过来的时候,flask 会先创建当前线程或者进程需要处理的两个重要上下文对象,把它们保存到隔离的栈里面,这样视图函数进行处理的时候就能直接从栈上获取这些信息。
结合上篇文章,Flask整个流程就很明确了。
一个线程同时只处理一个请求,那么 _req_ctx_stack和 _app_ctx_stack肯定都是只有一个栈顶元素的,为什么还要栈这种数据结构?
每个请求同时拥有这两个上下文信息,为什么要把 request context 和 application context 分开?
为什么要使用栈这种数据结构
因为Flask可以有多个应用,也就是在一个 Python 进程中,可以拥有多个应用,如果是多个 app,那么栈顶存放的是当前活跃的 request,也就是说使用栈是为了获取当前的活跃 request 对象。
为什么要拆分请求上下文和应用上下文
为了灵活度,为了可以让我们单独创建两个上下文,以便应付不同的场景。