前言
上一篇中我們已經(jīng)知道flask運(yùn)行的大體流程(Flask的工作原理)民镜,其中進(jìn)入wsgi_app中首先創(chuàng)建的就是上下文環(huán)境乍桂,那么什么是上下文呢袜瞬,又有什么作用怜俐。在了解上下文之前,先要弄清楚LocalProxy邓尤,Local拍鲤,LocalStack這三個(gè)概念。
Local
根據(jù)werkzeug文檔介紹汞扎,local是提供了線程隔離的數(shù)據(jù)訪問方式季稳,類似于python中的 thread locals∨謇蹋可以理解為存在一個(gè)全局的數(shù)據(jù)绞幌,不同線程可以讀取和修改它。不同線程之間是彼此隔離的一忱。
什么是線程隔離呢莲蜘?
比如存在一個(gè)全局變量數(shù)字10,在線程1中把10改為1帘营,主線程中讀取這個(gè)數(shù)字票渠,發(fā)現(xiàn)數(shù)字變成了1,也就是說新線程數(shù)據(jù)影響了主線程數(shù)據(jù)芬迄。這樣一來问顷,多線程之間考慮其他線程帶來的影響,從而不能安全地讀取和修改數(shù)據(jù)禀梳。
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)
結(jié)果
1
為什么不使用python thread local呢杜窄?因?yàn)檫@個(gè)有一些缺陷,比如
- 有些應(yīng)用使用的是greenlet協(xié)程算途,這種情況下無法保證協(xié)程之間數(shù)據(jù)的隔離塞耕,因?yàn)椴煌膮f(xié)程可以在同一個(gè)線程當(dāng)中。
- 即使使用的是線程嘴瓤,WSGI應(yīng)用也無法保證每個(gè)http請求使用的都是不同的線程扫外,因?yàn)楹笠粋€(gè)http請求可能使用的是之前的http請求的線程莉钙,這樣的話存儲于thread local中的數(shù)據(jù)可能是之前殘留的數(shù)據(jù)。
而Local解決了上面的問題筛谚,實(shí)現(xiàn)了線程之間的數(shù)據(jù)隔離磁玉,從而能夠安全的讀取和修改數(shù)據(jù)。
werkzeug中Local的實(shí)現(xiàn)
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的實(shí)現(xiàn)方式是使用python中的dict結(jié)構(gòu)驾讲,根據(jù)每個(gè)線程的id去創(chuàng)建獨(dú)立的數(shù)據(jù)蚊伞,當(dāng)訪問數(shù)據(jù)時(shí),根據(jù)當(dāng)前線程的id進(jìn)行訪問蝎毡,每個(gè)線程訪問自己的數(shù)據(jù)即可厚柳。這樣就實(shí)現(xiàn)了線程隔離的數(shù)據(jù)訪問。
Local引入了get_ident方法用于獲取當(dāng)前線程的id沐兵,將get_ident方法存儲至類的__ident_func__屬性中,將所有線程的數(shù)據(jù)存儲至__storage__屬性中便监,此屬性對應(yīng)的是一個(gè)二維dict扎谎,每個(gè)線程使用一個(gè)dict存儲數(shù)據(jù),而每個(gè)線程的dict數(shù)據(jù)作為__storage__的線程id對應(yīng)的值烧董。因此線程訪問數(shù)據(jù)事實(shí)上是訪問__storage__[ident][name]毁靶,其中前面的ident為線程的id,后面name才是用戶指定的數(shù)據(jù)key逊移。而ident是Local自動(dòng)獲取的预吆,用戶可以透明進(jìn)行線程隔離的數(shù)據(jù)訪問與存儲。
Local使用
我們可以單獨(dú)使用Local胳泉,實(shí)現(xiàn)線程隔離拐叉。
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))
結(jié)果
線程1中的a的值為:1
主線程中的a的值為:10
obj是一個(gè)線程隔離的對象,所以線程1的改變沒有影響到主線程扇商。
LocalStack
LocalStack是一個(gè)多線程隔離棧結(jié)構(gòu)凤瘦,通過源碼發(fā)現(xiàn)是基于Local實(shí)現(xiàn)的,提供了棧結(jié)構(gòu)push案铺,pop蔬芥,top方法。
class LocalStack(object):
def __init__(self):
self._local = Local()
def __release_local__(self):
self._local.__release_local__()
# 獲取線程對應(yīng)的id控汉,直接復(fù)用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也可以單獨(dú)使用,可以理解為就是普通的棧結(jié)構(gòu),只是這個(gè)棧結(jié)構(gòu)是數(shù)據(jù)安全的姑子。
import threading
from werkzeug.local import LocalStack
ls = LocalStack()
ls.push(1)
def worker1():
"""線程1"""
print('線程1中的棧頂?shù)闹禐椋簕}'.format(ls.top))
ls.push(2)
print('線程1中的棧頂?shù)闹禐椋簕}'.format(ls.top))
t1 = threading.Thread(target=worker1, name='線程1')
t1.start()
t1.join()
print('主線程中的棧頂?shù)闹禐椋簕}'.format(ls.top))
結(jié)果
線程1中的棧頂?shù)闹禐椋篘one
線程1中的棧頂?shù)闹禐椋?
主線程中的棧頂?shù)闹禐椋?
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來獲取代理的對象,然后執(zhí)行相應(yīng)的操作即可壁酬。對proxy執(zhí)行的任意操作次酌,都是直接通過被代理對象執(zhí)行的恨课。為了保證代理對象可以直接進(jìn)行操作,LocalProxy重載了所有的基本方法岳服,這樣就可以隨意對proxy對象執(zhí)行操作剂公。
那么為什么需要LocalProxy來代理Local或LocalStack對象呢?
LocalProxy的作用
下面看一個(gè)例子吊宋,直接使用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()
# 直接調(diào)用函數(shù)獲取user對象
user = get_user()
print(user['name'])
print(user['name'])
結(jié)果
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'])
結(jié)果
John
Bob
結(jié)果顯而易見,直接使用LocalStack對象璃搜,user一旦賦值就無法再動(dòng)態(tài)更新了拖吼,而使用Proxy,每次調(diào)用操作符这吻,都會(huì)重新獲取user吊档,從而實(shí)現(xiàn)了動(dòng)態(tài)更新user的效果。
而在_get_current_object方法中可以看到唾糯,對于可執(zhí)行對象或方法怠硼,就是直接執(zhí)行獲取可執(zhí)行對象或方法對應(yīng)的返回值。而對于Local對象移怯,則是獲取name作為屬性的值香璃。但是要注意的是,所有的獲取都是在執(zhí)行操作的時(shí)候獲取的舟误,這樣就可以隨著程序運(yùn)行動(dòng)態(tài)更新葡秒。同樣也可以解釋了上面的例子中,為方法get_user創(chuàng)建的LocalProxy類型的proxy_user嵌溢,可以兩次執(zhí)行proxy_user[‘name’]獲取到不同值了眯牧,因?yàn)閮纱螆?zhí)行時(shí),都會(huì)通過_get_current_object執(zhí)行g(shù)et_user方法堵腹,兩次執(zhí)行的結(jié)果不同炸站,返回的值也就不同了。
了解了Local疚顷,LocalProxy旱易,LocalStack,接下來看一下上下文腿堤。
什么是上下文
上下文多用于文章中阀坏,代表的一個(gè)整體環(huán)境,比如一篇文章笆檀,我們可以說下文中忌堂,訪問到下文所陳述的內(nèi)容,也可以說上文中酗洒,訪問到上文中的內(nèi)容士修,而我們這篇文章中每一段文字所代表的意思枷遂,都是要根據(jù)我們的上下文來決定的,因?yàn)槟汶S便拿出來一句話不去結(jié)合整體的語境去理解出來的意思肯定不是準(zhǔn)確的棋嘲,所以酒唉,我們這篇文章的上下文就是我們整篇的中心思想。
這是文章中的上下文沸移,那么程序中的上下文是什么痪伦?
程序中的上下文代表了程序當(dāng)下所運(yùn)行的環(huán)境,存儲了一些程序運(yùn)行的信息雹锣。比如在程序中我們所寫的函數(shù)大都不是單獨(dú)完整的网沾,在使用一個(gè)函數(shù)完成自身功能的時(shí)候,很可能需要同其他的部分進(jìn)行交互蕊爵,需要其他外部環(huán)境變量的支持辉哥,上下文就是給外部環(huán)境的變量賦值,使函數(shù)能正確運(yùn)行攒射。
Flask中的上下文
flask中有兩個(gè)上下文证薇,請求上下文和應(yīng)用上下文。
請求上下文
在 flask 中匆篓,可以直接在視圖函數(shù)中使用 request 這個(gè)對象進(jìn)行獲取相關(guān)數(shù)據(jù),而 request 就是請求上下文的對象寇窑,保存了當(dāng)前本次請求的相關(guān)數(shù)據(jù)鸦概。請求上下文對象有:request、session甩骏。
- request
封裝了HTTP請求的內(nèi)容窗市,針對的是http請求。舉例:user = request.args.get('user')饮笛,獲取的是get請求的參數(shù)咨察。 - session
用來記錄請求會(huì)話中的信息,針對的是用戶信息福青。舉例:session['name'] = user.id摄狱,可以記錄用戶信息。還可以通過session.get('name')獲取用戶信息无午。
應(yīng)用上下文
flask 應(yīng)用程序運(yùn)行過程中媒役,保存的一些配置信息,比如程序名宪迟、數(shù)據(jù)庫連接酣衷、應(yīng)用信息等。應(yīng)用上下文對象有:current_app次泽,g
- current_app
應(yīng)用程序上下文,用于存儲應(yīng)用程序中的變量穿仪,可以通過current_app.name打印當(dāng)前app的名稱席爽,也可以在current_app中存儲一些變量,例如:
應(yīng)用的啟動(dòng)腳本是哪個(gè)文件啊片,啟動(dòng)時(shí)指定了哪些參數(shù)
加載了哪些配置文件只锻,導(dǎo)入了哪些配置
連了哪個(gè)數(shù)據(jù)庫
有哪些public的工具類、常量
應(yīng)用跑再哪個(gè)機(jī)器上钠龙,IP多少炬藤,內(nèi)存多大 - g變量
g 作為 flask 程序全局的一個(gè)臨時(shí)變量,充當(dāng)者中間媒介的作用,我們可以通過它傳遞一些數(shù)據(jù),g 保存的是當(dāng)前請求的全局變量碴里,不同的請求會(huì)有不同的全局變量沈矿。
current_app 的生命周期最長,只要當(dāng)前程序?qū)嵗€在運(yùn)行咬腋,都不會(huì)失效羹膳。
request 和 g 的生命周期為一次請求期間,當(dāng)請求處理完成后根竿,生命周期也就完結(jié)了陵像。
curren_app,g,request,session都是線程隔離的,我們可通過源碼發(fā)現(xiàn)寇壳。
上下文的定義
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"))
這里定義了兩個(gè)LocalStack棧醒颖,_request_ctx_stack是請求上下文棧,_app_ctx_stack是應(yīng)用上下文棧壳炎,curren_app,g,request,session都是LocalStack棧頂元素泞歉。
上下文處理流程
在上一篇中(Flask的工作原理)我們看到wsgi_app中會(huì)創(chuàng)建上下文環(huán)境,調(diào)用 ctx.push() 函數(shù)將上下文信息壓棧匿辩。
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)實(shí)際上是實(shí)例化一個(gè)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()是創(chuàng)建上下文環(huán)境,把該請求的 ApplicationContext 和 RequestContext 有關(guān)的信息保存到對應(yīng)的棧上铲球。
到了這里Flask上下文基本明確了挺庞,每次有請求過來的時(shí)候,flask 會(huì)先創(chuàng)建當(dāng)前線程或者進(jìn)程需要處理的兩個(gè)重要上下文對象稼病,把它們保存到隔離的棧里面选侨,這樣視圖函數(shù)進(jìn)行處理的時(shí)候就能直接從棧上獲取這些信息。
結(jié)合上篇文章溯饵,F(xiàn)lask整個(gè)流程就很明確了侵俗。
一個(gè)線程同時(shí)只處理一個(gè)請求,那么 _req_ctx_stack和 _app_ctx_stack肯定都是只有一個(gè)棧頂元素的丰刊,為什么還要棧這種數(shù)據(jù)結(jié)構(gòu)隘谣?
每個(gè)請求同時(shí)擁有這兩個(gè)上下文信息,為什么要把 request context 和 application context 分開?
為什么要使用棧這種數(shù)據(jù)結(jié)構(gòu)
因?yàn)镕lask可以有多個(gè)應(yīng)用寻歧,也就是在一個(gè) Python 進(jìn)程中掌栅,可以擁有多個(gè)應(yīng)用,如果是多個(gè) app码泛,那么棧頂存放的是當(dāng)前活躍的 request猾封,也就是說使用棧是為了獲取當(dāng)前的活躍 request 對象。
為什么要拆分請求上下文和應(yīng)用上下文
為了靈活度噪珊,為了可以讓我們單獨(dú)創(chuàng)建兩個(gè)上下文晌缘,以便應(yīng)付不同的場景。