1. 程式人生 > >Flask 上下文機制和執行緒隔離

Flask 上下文機制和執行緒隔離

> **1. 電腦科學領域的任何問題都可以通過增加一個間接的中間層來解決**, 上下文機制就是這句話的體現。 > > **2. 如果一次封裝解決不了問題,那就再來一次** 上下文:相當於一個容器,儲存了Flask程式執行過程中的一些資訊 原始碼:[flask/ctx.py](https://github.com/pallets/flask/blob/master/src/flask/ctx.py) - 請求上下文:Flask從客戶端收到請求時,要讓檢視函式能訪問一些物件,這樣才能處理請求,要想讓檢視函式能夠訪問請求物件,一個顯而易見的方式是將其作為引數傳入檢視函式,不過這會導致程式中的每個檢視函式都增加一個引數,除了訪問請求物件,如果檢視函式在處理請求時還要訪問其他物件,情況會變得更糟。為了避免大量可有可無的引數把檢視函式弄得一團糟,Flask使用上下文臨時把某些物件變為全域性可訪問。**這就是一種重構設計思路**。 - **request** 封裝了HTTP請求的內容,針對http請求,也是一種符合WSGI介面規範的設計(**關於WSGI可參考我對該協議的理解和實現demo [mini-wsgi-web](https://github.com/Panlq/Py-Project/tree/master/mini-web)**),如 `request.args.get('user')` - **session** 用來記錄請求會話中的資訊,針對的是使用者資訊,如 `session['name'] = user.id` - 應用上下文:應用程式上下文,用於儲存應用程式中的變數 - **current_app** 儲存應用配置,資料庫連線等應用相關資訊 - **g變數** 作為flask程式全域性的一個**臨時變數**, 充當者中間媒介的作用,我們可以通過它傳遞一些資料,g儲存的是當前請求的全域性變數,**不同的請求會有不同的全域性變數,通過不同的thread id區別** ```python # 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 ```python #!/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 '

Hello World. Have a nice day!
' if __name__ == '__main__': app.run(host='localhost', port=8888) ``` > 報錯: > > Exception has occurred: RuntimeError > > Working outside of application context. ![](https://img2020.cnblogs.com/blog/778496/202007/778496-20200708133619522-1637712399.png) ## 2. flask 上下文出入棧 **flask上下文物件出入棧模型圖** ![](https://img2020.cnblogs.com/blog/778496/202007/778496-20200708154936922-1740856449.png) **在應用開發中可用直接引用`current_app`不會報錯,是因為當在一個請求中使用的時候,flask會判斷`_app_ctx_stack`棧頂是否有可用物件,如果沒有就會自動推入一個App**. 我們獲取的`current_app`就是獲取的棧頂元素 ```python # 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物件推入棧頂 ```python # ... # 將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'] ``` ![](https://img2020.cnblogs.com/blog/778496/202007/778496-20200708155134961-419020986.png) 不在出現`unbound`狀態。可正常執行 既然flask會自動幫我們檢測棧頂元素是否存在,為什麼我們還要做這一步操作,**當我們在寫離線應用,或者單元測試的時候就需要用到,因為請求是模擬的,不是在application context中的了。** ## 3. python中的上文管理器 實現了`__enter__`和`__exit__`方法的物件就是一個上文管理器。 1. **實現了`__enter__`和`__exit__`方法的物件就是一個上文管理器。** ```python 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`** 2. **另一種實現上下文管理器的方法是`contextmanager` 裝飾器** 使用 `contextmanager `的裝飾器,可以簡化上下文管理器的實現方式。原理是通過 `yield` 將函式分割成兩部分,`yield` 之前的語句在`__enter__` 方法中執行,`yield` 之後的語句在`__exit__` 方法中執行。緊跟在 `yield` 後面的值是函式的返回值。 ```python from contextlib import contextmanager class MyResource: def query(self): print('query data') @contextmanager def my_resource(): print('connect ro resource') yield MyResource() print('close resource connection') try: with my_resource() as r: r.query() except Exception as e: print(e) ``` 兩種方法並沒有說哪一種方法好,各有優略,看程式碼環境的使用場景來決定。`contextmanager` 以下就是一個簡單的需求,給文字加書名號。 ```python @contextmanager def book_mark(): print('《', end='') yield print('》', end='') with book_mark(): print('你還年輕? peer已經年少有為!', end='') ``` **實際應用場景**,封裝一些公用方法。 原本業務邏輯中是這樣的, 在所有的模型類中,在新建資源的時候都需要`add`, `commit`, 或者`rollback`的操作。 ```python @login_required def save_to_gifts(isbn): if current_user.can_save_to_list(isbn): try: gift = Gift() gift.isbn = isbn gift.uid = current_user.id db.session.add(gift) db.session.commit() except Exception as e: db.session.rollback() raise e ``` 簡化後 ```python @login_required def save_to_gifts(isbn): if current_user.can_save_to_list(isbn): with db.auto_commit(): gift = Gift() gift.isbn = isbn gift.uid = current_user.id db.session.add(gift) ``` 其中資料庫的封裝如下 ```python #!/usr/bin/python3 # -*- coding: utf-8 -*- # __author__ = '__JonPan__' from contextlib import contextmanager from flask_sqlalchemy import SQLAlchemy as _SQLAlchemy, BaseQuery # 抽離公用程式碼,with 形式來完成db.commit rollback class SQLAlchemy(_SQLAlchemy): @contextmanager def auto_commit(self): try: yield self.session.commit() except Exception as e: self.session.rollback() raise e class Query(BaseQuery): def filter_by(self, **kwargs): if 'status' not in kwargs.keys(): kwargs['status'] = 1 return super(Query, self).filter_by(**kwargs) db = SQLAlchemy(query_class=Query) ``` ## 執行緒隔離機制 ![](https://img2020.cnblogs.com/blog/778496/202007/778496-20200708155056775-1634773789.png) **如何實現一個Reqeust 指向多個請求例項,且要區分該例項物件所繫結的使用者?** 字典: >
request = {'key1': val1, 'key2': val2} flask引用 `werkzeug` 中的 `local.Local` 實現執行緒隔離 ### Local ```python # 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 ```python 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實現了執行緒隔離的棧結構 ## 總結 1. flask上下文的實現使用了設計模式中的代理模式,`current_app`, `requsts`, 等代理物件都是執行緒隔離的, 當我們啟動一個執行緒去執行一個非同步操作需要用到應用上下文時(需傳入`app`物件), 如果傳入`current_app`, 此時的`app` 時` unbond`的狀態, 由於執行緒id改變了, 所以在新的執行緒中 所有的棧都是空的, 但是在整個`web`中 由Flask 例項化的 `app`是唯一的, 所以獲取app傳入是可以的 `app = current_app._get_current_object()` 2. 執行緒隔離的實現機制就是利用執行緒ID+字典, 使用執行緒隔離的意義在於:使當前執行緒能夠正確引用到他自己所建立的物件,而不是引用到其他執行緒所建立的物件 3. `current_app -> (LocalStack.top = AppContext top.app = Flask)` 4. `request -> (LocalStack.top = RequestContext.top.request = Request)` ## 參考資料 [Python Flask高階程式設計-七月](https://coding.imooc.com/class/194.html)