Werkzeug 作為一個 WSGI 工具庫,由於一些方面的考慮,並沒有直接使用python內建的ThreadLocal類,而是自己實作了一系列Local類。包括簡單的Local,以及在此基礎上實現的LocalStack,LocalManager 和 LocalProxy。接下來我們一起來看看這些類別的使用方式,設計的初衷,以及具體的實作技巧。
Local 類的設計
Werkzeug 的設計者認為python自帶的ThreadLocal並不能滿足需求,主要因為下面兩個原因:
Werkzeug 主要用「ThreadLocal」來滿足並發的要求,python 附帶的Threadthal能實現基於線程的並發。而python還有其他許多並發方式,例如常見的協程(greenlet),因此需要實作一種能夠支援協程的Local物件。
WSGI不保證每次都會產生一個新的線程來處理請求,也就是說線程是可以復用的(可以維護一個線程池來處理請求)。這樣如果werkzeug 使用python自帶的ThreadLocal,一個「不乾淨(存有之前處理過的請求的相關資料)」的線程會被用來處理新的請求。
為了解決這兩個問題,werkzeug 中實作了Local類別。 Local物件可以做到執行緒和協程之間資料的隔離,此外,還要支援清理某個執行緒或協程下的資料(這樣就可以在處理一個請求之後,清理對應的數據,然後等待下一個請求的到來)。
具體怎麼實現的呢,思想其實特別簡單,我們在深入理解Python中的ThreadLocal變數(上)一文的最後有提起過,就是創建一個全局字典,然後將線程(或者協程)標識符作為key ,對應線程(或協程)的局部資料作為value。這裡 werkzeug 就是按照上面思路進行實現,不過利用了python的一些黑魔法,最後提供給用戶一個清晰、簡單的接口。
具體實作
Local 類別的實作在 werkzeug.local 中,以 8a84b62 版本的程式碼進行分析。透過前兩篇對ThreadLocal的了解,我們已經知道了Local物件的特色與使用方法。所以這裡不再給Local物件的使用例子,我們直接看程式碼。
class Local(object): __slots__ = ('__storage__', '__ident_func__') def __init__(self): object.__setattr__(self, '__storage__', {}) object.__setattr__(self, '__ident_func__', get_ident) ...
由於可能有大量的Local對象,為了節省Local對象佔用的空間,這裡使用__slots__ 寫死了Local可以擁有的屬性:
__storage__: 值為一個字典,用來保存實際的數據,初始化為空;
__ident_func__:值為一個函數,用來找出目前執行緒或協程的標誌符。
由於Local物件實際的資料保存在__storage__中,所以對Local屬性的操作其實是對__storage__的操作。對於取得屬性而言,這裡用魔術方法__getattr__攔截__storage__ 和 __ident_func__以外的屬性獲取,將其導向__storage__儲存的目前執行緒或協程的資料。而對於屬性值的set或del,則分別用__setattr__和__setattr__來實現(這些魔術方法的介紹見屬性控制)。關鍵程式碼如下所示:
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)
假設我們有ID為1,2, ... , N 的N個線程或協程,每個都用Local物件保存有自己的一些局部數據,那麼Local物件的內容如下圖所示:
此外,Local類別還提供了__release_local__方法,用來釋放當前線程或協程保存的資料。
Local 擴充介面
Werkzeug 在 Local 的基礎上實作了 LocalStack 和 LocalManager,用來提供更友善的介面支援。
LocalStack
LocalStack通過封裝Local從而實現了一個線程(或者協程)獨立的棧結構,註釋裡面有具體的使用方法,一個簡單的使用例子如下
ls = LocalStack() ls.push(12) print ls.top # 12 print ls._local.__storage__ # {140735190843392: {'stack': [12]}}
LocalStack 的實現比較有意思,它將一個Local物件作為自己的屬性_local,然後定義介面push, pop 和top 方法進行對應的堆疊操作。這裡用 _local.__storage__._local.__ident_func__() 這個list來模擬堆疊結構。在介面push, pop和top中,透過操作這個list來模擬堆疊的操作,需要注意的是在介面函數內部取得這個list時,不用像上面黑體那麼複雜,可以直接用_local的getattr()方法即可。以 push 函數為例,實作如下:
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
pop 和 top 的實作和一般堆疊類似,都是對 stack = getattr(self._local, 'stack', None) 這個列表進行對應的操作。此外,LocalStack還允許我們自訂__ident_func__,這裡用內建函數property 產生了描述器,封裝了__ident_func__的get和set操作,提供了一個屬性值__ident_func__作為接口,具體代碼如下:
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__
LocalManager
Local 和LocalStack 都是線程或協程獨立的單一對象,很多時候我們需要一個線程或協程獨立的容器,來組織多個Local或LocalStack對象(就像我們用一個list來組織多個int或string類型一樣)。
Werkzeug实现了LocalManager,它通过一个list类型的属性locals来存储所管理的Local或者LocalStack对象,还提供cleanup方法来释放所有的Local对象。Werkzeug中LocalManager最主要的接口就是装饰器方法make_middleware,代码如下:
def make_middleware(self, app): """Wrap a WSGI application so that cleaning up happens after request end. """ def application(environ, start_response): return ClosingIterator(app(environ, start_response), self.cleanup) return application
这个装饰器注册了回调函数cleanup,当一个线程(或者协程)处理完请求之后,就会调用cleanup清理它所管理的Local或者LocalStack 对象(ClosingIterator 的实现在 werkzeug.wsgi中)。下面是一个使用 LocalManager 的简单例子:
from werkzeug.local import Local, LocalManager local = Local() local_2 = Local() local_manager = LocalManager([local, local2]) def application(environ, start_response): local.request = request = Request(environ) ... # application 处理完毕后,会自动清理local_manager 的内容
通过LocalManager的make_middleware我们可以在某个线程(协程)处理完一个请求后,清空所有的Local或者LocalStack对象,这样这个线程又可以处理另一个请求了。至此,文章开始时提到的第二个问题就可以解决了。Werkzeug.local 里面还实现了一个 LocalProxy 用来作为Local对象的代理,也非常值得去学习。