认识Flask的上下文流程,需要知道threading.local()的原理
线程数据安全的三种情况:
情况一: 单进程单线程, 基于全局变量
情况二: 单进程多线程, threading.local实现线程数据隔离
情况三: 单进程单线程(携程), threading.local不能实现数据隔离, 但基于threading.local的思想,通过获取携程的唯一标识, 创建字典的形式存储数据
数据形式如:
{
1368 : { k : v }
1339 : { k : v }
...
}
模仿threading.local的思想,手动实现获取线程唯一标识存储数据:
import threading
from _thread import get_ident
class Local():
def __init__(self):
# 定义一个空字典, 用于存储不同线程下的数据
self.storage = { }
# 将get_ident方法赋值到Local对象中,后续获取线程唯一标识使用
self.get_ident = get_ident
# 设置数据
def set(self, k, v):
ident = self.get_ident() # 获取线程标识
origin = self.storage.get(ident) # 获取线程的数据字典
if not origin: # 若origin为空,则说明该线程还没有数据
origin = {k:v} # 生成该线程的数据字典
else:
origin[k] = v # 若origin不为空,则说明该线程以及有了数据字典,只需要把新的数据添加到字典中
self.storage[ident] = origin # 将线程标识和数据字典添加到storage这个大字典中
# 获取数据
def get(self, k):
ident = self.get_ident() # 获取线程标识
origin = self.storage.get(ident) # 获取线程的数据字典
if not origin:
return None
else:
v = origin.get(k, None)
return v
local_value = Local()
def task(num):
local_value.set('name', num) # 为各自的线程空间赋值
import time
time.sleep(1)
print(local_value.get('name'), threading.current_thread().name)
for i in range(5):
th = threading.Thread(target=task, args=(i,), name='线程%s' % i)
th.start()
基于上面的思想,把获取线程唯一标识换成获取携程的唯一标识,为每个携程存储数据
如何获取携程唯一标识?
pip install gevent # 安装gevent模块时自动安装greenlet模块
from greenlet import getcurrent as get_ident # 导入getcurrent方法,该方法可以获取携程唯一标识
手动实现获取携程程唯一标识存储数据:
与上面的获取线程唯一标识思路一样
import threading
# 支持携程优先使用携程,否则使用线程
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():
def __init__(self):
# 定义一个空字典, 用于存储不同线程下的数据
self.storage = { }
# 将get_ident方法赋值到Local对象中,后续获取线程唯一标识使用
self.get_ident = get_ident
# 设置数据
def set(self, k, v):
ident = self.get_ident() # 获取线程标识
origin = self.storage.get(ident) # 获取线程的数据字典
if not origin: # 若origin为空,则说明该线程还没有数据
origin = {k:v} # 生成该线程的数据字典
else:
origin[k] = v # 若origin不为空,则说明该线程以及有了数据字典,只需要把新的数据添加到字典中
self.storage[ident] = origin # 将线程标识和数据字典添加到storage这个大字典中
# 获取数据
def get(self, k):
ident = self.get_ident() # 获取线程标识
origin = self.storage.get(ident) # 获取线程的数据字典
if not origin:
return None
else:
v = origin.get(k, None)
return v
local_value = Local()
def task(num):
local_value.set('name', num) # 为各自的线程空间赋值
import time
time.sleep(1)
print(local_value.get('name'), threading.current_thread().name)
for i in range(5):
th = threading.Thread(target=task, args=(i,), name='线程%s' % i)
th.start()
Flask上下文源码 之 LocalStack()
补充知识:
构造函数内有两种为对象赋值的方法
def __init__(self)
self.name = { }
object.__setattr__(self, 'name', { }) # 该方法通常在类中定义__setattr__方法时使用(防止递归)
LocalStack() 中实例化 Local(), Local()与 threading.local的整体思路一致
_request_ctx_stack = LocalStack() # 点进去查看到下面的代码
try:
# 判断是否支持携程, 若支持则优先使用携程,并导入getcurrent, 用于获取携程唯一标识
from greenlet import getcurrent as get_ident
except ImportError:
try:
# 若不支持携程,则使用线程, 并导入thread, 用于获取获取线程唯一标识
from thread import get_ident
except ImportError:
from _thread import get_ident
说明:
python2中是 thread
python3中是 _thread
class LocalStack(object):
def __init__(self):
self._local = Local() # 实例化Local()
......
class Local(object):
__slots__ = ("__storage__", "__ident_func__")
def __init__(self):
object.__setattr__(self, "__storage__", {}) # self.__storage__ = { }
object.__setattr__(self, "__ident_func__", get_ident) # 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 # 将各自线程的数据存到各自的数据字典中 {ident:{'key':'value'}}
except KeyError:
storage[ident] = {name: value} # 若该线程之前不存在,则创建该线程的数据字典
def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
上下文流程:
class Flask()
...
1.
def run(self, host=None, port=None, debug=None, load_dotenv=True, **options):
...
from werkzeug.serving import run_simple
try:
2.
run_simple(host, port, self, **options) # 这里的 self 是 Flask 对象, 这里的self会加括号执行Flask()里的__call__方法
finally:
...
4.
def wsgi_app(self, environ, start_response):
# 将请求相关的数据environ封装到了request_context中的RequestContext类中
ctx = self.request_context(environ) # ctx是RequestContext的对象, ctx包含了所有请求相关的数据
...
5.
def request_context(self, environ):
return RequestContext(self, environ) # self: 是Flask对象app
3.
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response) # environ: 是基于HTTP请求的所有数据(请求首行,头,体)
1. app.run 触发 Flask()中的run方法
2. run方法中核心是导入 from werkzeug.serving import run_simple 触发
run_simple(host, port, self, **options) 注意,这里的 self 是 Flask 对象, 这里的self会加括号执行Flask()里的__call__方法
3.Flask()里的__call__才是入口, 这里调用了wsgi_app方法
4.Flask()里的wsgi_app方法封装了请求的所有信息,封装到了RequestContext类中
请求上下文:
通过上面的上下文流程,能找到请求相关的数据别封装到了RequestContext中
1.请求来 将请求数据添加到对应的线程数据字典中
2.请求走 将请求数据从对应的线程数据字典中删除
class Flask():
...
1.
def wsgi_app(self, environ, start_response):
# 将请求相关的数据environ封装到了request_context中
2.
ctx = self.request_context(environ) # ctx是RequestContext的对象
error = None
try:
try:
3.
ctx.push() # ctx调用push方法, push是对象绑定方法, 默认将ctx传入
...
finally:
...
7.
ctx.auto_pop(error)
class RequestContext(object):
...
4.
def push(self):
...
5.
_request_ctx_stack.push(self) # self是ctx _request_ctx_stack是LocalStack()的对象
# 将请求相关的数据push到_request_ctx_stack中
class LocalStack(object):
def __init__(self):
self._local = Local() # Local()核心是为每个线程或者携程创建自己的数据存储空间
6. 将请求信息放到各自线程的数据字典中
def push(self, obj): # 这里的obj是ctx对象, 请求相关的所有数据
rv = getattr(self._local, "stack", None) # self._local是 Local()对象 补充:{唯一标识: {'stack':[ctx,]}}
if rv is None:
self._local.stack = rv = [] # 这里触发Local()中的__setattr__方法
rv.append(obj) # 将ctx放到Local()对象中
return rv
8. 将请求信息从各自线程的数据字典中提出
def pop(self):
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()
问: 谈谈Flask上下文管理
答:
1.首先解释threading.local的原理, Flask为是支持携程,基于threading.local的思想,自己实现Loacl类,在Local类中通过获取携程唯一标识,存储携程自己的数据信息
2.请求刚进来触发run_simple中的Flask对象,进而触发Flask对象的__call__方法,调用wsgi_app方法
2.1 首先封装了请求的所有信息到RequestContext中,这里封装了两层,一层是RequestContext, 另一层是request_class(注意: RequestContext里还封装了session)
2.2 RequestContext对象ctx.push,将请求相关的数据push到_request_ctx_stack中,即LocalStack()中
2.3 LocalStack()中实例化了Loacl(), 将请求相关的数据存到各自的线程数据字典中
3. 当我们导入使用request时, 调用的是全局的request对象,这个request对象是LocalProxy实例化的,当使用request里的方法(如: request.form)时, 会调用LocalProxy中的_get_current_object方法去Loacl()中将RequestContext的对象拿出来(RequestContext封装了请求的数据),最终就是 ctx.request.form, 核心就是拿到RequestContext的对象,即ctx
4. 当请求走的时候,会执行ctx.auto_pop方法调用LocalStack()中的pop方法,将Loacl()中存储的RequestContext对象删除
应用上下文
问: 请求扩展里的 before_request 会在请求到达视图函数前执行, 那么,在before_request中设置一个值, 把这个值传给视图函数的办法
答: 可以将这个值赋值到 request 中, 或赋值到 session 中, 这种方法可以, 但是很有可能这个值会与request或session中的数据冲突,
所以,基于以上问题, Flask中提供了一个 g对象, g对象是一个全局对象, 可以将上述问题的值赋值到g对象中
介绍: g对象是一个容器,每个请求周期都会创建一个g,用于在请求周期中传递值时存值使用
注意: g对象赋的值,在一个周期内可使用
案例:
from flask import Flask, request, g
app = Flask(__name__)
@app.before_request
def before():
# 设置值
request.xxx = 123
g.xxx = 123
pass
@app.route('/', methods=['GET', ])
def index():
# 获取值
print(request.xxx)
print(g.xxx)
return 'index'
if __name__ == '__main__':
app.run()
源码:
首先一步步找到wsgi_app方法
def wsgi_app(self, environ, start_response):
ctx = self.request_context(environ) # ctx是RequestContext的对象
error = None
try:
try:
1.
ctx.push() # 这是核心请求上下文和应用上下文都在这里开始
finally:
...
9.
ctx.auto_pop(error)
class RequestContext(object):
2.
def push(self):
...
if app_ctx is None or app_ctx.app != self.app:
# 应用上下文入口:
# app_context里面实例化AppContext(), 创建了 g 对象, g相当是一个全局字典
3.
app_ctx = self.app.app_context() # app_ctx就是AppContext()的对象
4.
app_ctx.push() # 调用AppContext()中的push方法
class AppContext(object):
def __init__(self, app):
self.app = app
self.url_adapter = app.create_url_adapter(None)
5. app_ctx中创建g对象
self.g = app.app_ctx_globals_class()
6.
def push(self):
self._refcnt += 1
if hasattr(sys, "exc_clear"):
sys.exc_clear()
7. 调用LocalStack()中的push方法
_app_ctx_stack.push(self) # _app_ctx_stack是LocalStack()的对象
appcontext_pushed.send(self.app)
class LocalStack(object):
8.
def push(self, obj): # 这里的obj是ctx对象, 请求相关的所有数据
"""Pushes a new item to the stack"""
rv = getattr(self._local, "stack", None) # self._local是 Local()对象
if rv is None:
self._local.stack = rv = [] # 这里触发Local()中的__setattr__方法
rv.append(obj) # 将ctx放到Local()对象中
return rv
10.
def pop(self):
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()
'''
请求上下文存储形式:
_request_ctx_stack,local = {
'唯一标识': {'stack' : [ ctx, ]}
}
应用上下文存储形式:
_app_ctx_stack.local = {
'唯一标识': {'stack' : [ app_ctx, ]}
}
app_ctx = AppContext()
app_ctx对象里有 app, g
'''
问: 谈谈Flask应用上下文
答:
1.请求刚进来触发run_simple中的Flask对象,进而触发Flask对象的__call__方法,调用wsgi_app方法
2.首先封装了请求的所有信息到RequestContext中,这里封装了两层,一层是RequestContext, 另一层是request_class(注意: RequestContext里还封装了session)
3.RequestContext对象ctx.push,触发AppContext()类的实例化, 实例化时通过 app.app_ctx_globals_class() 创建了g对象
4.app_ctx 即AppContext()类的对象调用push方法,触发了 _app_ctx_stack.push方法,即LocalStack()类中push方法, 将app_ctx存储到各自线程的数据字典中
5.当请求走的时候,会执行ctx.auto_pop方法调用LocalStack()中的pop方法,将Loacl()中存储的AppContext()的对象删除
补充
Flask源码中 globals.py文件
def _lookup_req_object(name): # name 是 'request' 或者是 'session'
top = _request_ctx_stack.top # _app_ctx_stack.top的返回值是 ctx 对象
if top is None:
raise RuntimeError(_request_ctx_err_msg)
return getattr(top, name) # 从ctx中获取request 或者 获取session
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 # 等同于 ctx.app app是Flask对象
# context locals
_request_ctx_stack = LocalStack() # 主要功能 实例化Local(), 提供push方法,将请求信息放到各自线程的数据字典中. 提供pop方法, 将请求信息从各自线程的数据字典中提出
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
# 注意: 这里的偏函数里的 _lookup_req_object 会找到 ctx 对象
# 请求上下文
request = LocalProxy(partial(_lookup_req_object, "request")) # 目标: 去Local()中获取ctx, 在ctx中获取request
session = LocalProxy(partial(_lookup_req_object, "session")) # 目标: 去Local()中获取ctx, 在ctx中获取session
# 应用上下文
g = LocalProxy(partial(_lookup_app_object, "g"))