Flask 上下文機制和執行緒隔離
1. 電腦科學領域的任何問題都可以通過增加一個間接的中間層來解決, 上下文機制就是這句話的體現。
2. 如果一次封裝解決不了問題,那就再來一次
上下文:相當於一個容器,儲存了Flask程式執行過程中的一些資訊 原始碼:flask/ctx.py
請求上下文:Flask從客戶端收到請求時,要讓檢視函式能訪問一些物件,這樣才能處理請求,要想讓檢視函式能夠訪問請求物件,一個顯而易見的方式是將其作為引數傳入檢視函式,不過這會導致程式中的每個檢視函式都增加一個引數,除了訪問請求物件,如果檢視函式在處理請求時還要訪問其他物件,情況會變得更糟。為了避免大量可有可無的引數把檢視函式弄得一團糟,Flask使用上下文臨時把某些物件變為全域性可訪問。這就是一種重構設計思路
- request 封裝了HTTP請求的內容,針對http請求,也是一種符合WSGI介面規範的設計(關於WSGI可參考我對該協議的理解和實現demo mini-wsgi-web),如
request.args.get('user')
- session 用來記錄請求會話中的資訊,針對的是使用者資訊,如
session['name'] = user.id
- request 封裝了HTTP請求的內容,針對http請求,也是一種符合WSGI介面規範的設計(關於WSGI可參考我對該協議的理解和實現demo mini-wsgi-web),如
應用上下文:應用程式上下文,用於儲存應用程式中的變數
- current_app 儲存應用配置,資料庫連線等應用相關資訊
- g變數 作為flask程式全域性的一個臨時變數, 充當者中間媒介的作用,我們可以通過它傳遞一些資料,g儲存的是當前請求的全域性變數,不同的請求會有不同的全域性變數,通過不同的thread id區別
# context locals
# 使用代理模式 LocalProxy
_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'))
1. working outside application context
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# __author__ = '__JonPan__'
from flask import Flask, current_app
app = Flask(__name__)
a = current_app
# is_debug = current_app.config['DEBUG']
@app.route('/')
def index():
return '<h1>Hello World. Have a nice day! </1>'
if __name__ == '__main__':
app.run(host='localhost', port=8888)
報錯:
Exception has occurred: RuntimeError
Working outside of application context.
2. flask 上下文出入棧
flask上下文物件出入棧模型圖
在應用開發中可用直接引用current_app
不會報錯,是因為當在一個請求中使用的時候,flask會判斷_app_ctx_stack
棧頂是否有可用物件,如果沒有就會自動推入一個App. 我們獲取的current_app
就是獲取的棧頂元素
# flask/globals.py
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError(_app_ctx_err_msg)
return top.app
current_app = LocalProxy(_find_app)
修改程式碼:將app物件推入棧頂
# ...
# 將app_context 推入棧中
ctx = app.app_context()
ctx.push()
a = current_app
is_debug = current_app.config['DEBUG']
ctx.pop()
# ...
# 更有pythonnic的寫法
# 將app_context 推入棧中
# with app.app_context():
# a = current_app
# is_debug = current_app.config['DEBUG']
不在出現unbound
狀態。可正常執行
既然flask會自動幫我們檢測棧頂元素是否存在,為什麼我們還要做這一步操作,當我們在寫離線應用,或者單元測試的時候就需要用到,因為請求是模擬的,不是在application context中的了。
3. python中的上文管理器
實現了__enter__
和__exit__
方法的物件就是一個上文管理器。
class MyResource:
def __enter__(self):
print('connect ro resource')
return self
def __exit__(self, exc_type, exc_value, tb):
if tb:
print('process exception')
else:
print('no exception')
print('close resource connection')
# return True
# return False
def query(self):
print('query data')
try:
with MyResource() as r:
1/0
r.query()
except Exception as e:
print(e)
with MyResour ce() as r
as 的別名r指向的不是上想問管理器物件,而是__enter__
方法返回的值,在以上程式碼確實是返回了物件本身。
__exit__
方法 處理退出上下文管理器物件時的一些資源清理工作,並處理異常,三個引數
- exc_type 異常型別
- exc_value 異常原因解釋
- tb traceback
__exit__
其實是有返回值的,return True
表示,異常資訊已經在本方法中處理,外部可不接收異常,return False
表示將異常丟擲給上層邏輯處理,預設不寫返回,即預設值是None
, None
也表示False
執行緒隔離機制
如何實現一個Reqeust 指向多個請求例項,且要區分該例項物件所繫結的使用者?
字典:
request = {'key1': val1, 'key2': val2}
flask引用 werkzeug
中的 local.Local
實現執行緒隔離
# werkzeug\local.py
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):
# 獲取當前執行緒的id
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)
可以看到是使用執行緒ID來繫結不同的上線文物件。
LocalStack
class LocalStack(object):
"""This class works similar to a :class:`Local` but keeps a stack
of objects instead. This is best explained with an example::
>>> ls = LocalStack()
>>> ls.push(42)
>>> ls.top
42
>>> ls.push(23)
>>> ls.top
23
>>> ls.pop()
23
>>> ls.top
42
They can be force released by using a :class:`LocalManager` or with
the :func:`release_local` function but the correct way is to pop the
item from the stack after using. When the stack is empty it will
no longer be bound to the current context (and as such released).
By calling the stack without arguments it returns a proxy that resolves to
the topmost item on the stack.
.. versionadded:: 0.6.1
"""
def __init__(self):
self._local = Local()
def __release_local__(self):
self._local.__release_local__()
def _get__ident_func__(self):
return self._local.__ident_func__
def _set__ident_func__(self, value):
object.__setattr__(self._local, '__ident_func__', value)
__ident_func__ = property(_get__ident_func__, _set__ident_func__)
del _get__ident_func__, _set__ident_func__
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
Local使用字典的方式實現執行緒隔離,LocalStack 則封裝Local實現了執行緒隔離的棧結構
總結
- flask上下文的實現使用了設計模式中的代理模式
- 執行緒隔離的實現機制就是利用執行緒ID+字典, 使用執行緒隔離的意義在於:使當前執行緒能夠正確引用到他自己所建立的物件,而不是引用到其他執行緒所建立的物件
- 請求上下文, 應用上線文是執行緒隔離的物件,current_app並沒有使用執行緒隔離,全域性唯一
current_app -> (LocalStack.top = AppContext top.app = Flask)
request -> (LocalStack.top = RequestContext.top.request = Request)