一、簡介
在local
模組中,Werkzeug實現了類似Python標準庫中thread.local
的功能。thread.local
是執行緒區域性變數,也就是每個執行緒的私有變數,具有執行緒隔離性,可以通過執行緒安全的方式獲取或者改變執行緒中的變數。參照thread.local
,Werkzeug實現了比thread.local
更多的功能。Werkzeug官方文件關於local模組中對此進行了說明:
The Python standard library comes with a utility called “thread locals”. A thread local is a global object in which you can put stuff in and get back later in a thread-safe way. That means whenever you set or get an object on a thread local object, the thread local object checks in which thread you are and retrieves the correct value.
This, however, has a few disadvantages. For example, besides threads there are other ways to handle concurrency in Python. A very popular approach is greenlets. Also, whether every request gets its own thread is not guaranteed in WSGI. It could be that a request is reusing a thread from before, and hence data is left in the thread local object.
總結起來: 以上文件解釋了對於“併發”問題,多執行緒並不是唯一的方式,在Python中還有“協程”(關於協程的概念和用法可以參考:廖雪峰的部落格)。“協程”的一個顯著特點在於是一個執行緒執行,一個執行緒可以存在多個協程。也可以理解為:協程會複用執行緒。對於WSGI
應用來說,如果每一個執行緒處理一個請求,那麼thread.local
完全可以處理,但是如果每一個協程處理一個請求,那麼一個執行緒中就存在多個請求,用thread.local
變數處理起來會造成多個請求間資料的相互干擾。
對於上面問題,Werkzeug庫解決的辦法是local
模組。local
模組實現了四個類:
Local
LocalStack
LocalProxy
LocalManager
本文重點介紹前兩個類的實現。
二、Local類
Local
類能夠用來儲存執行緒的私有變數。在功能上這個thread.local
類似。與之不同的是,Local
類支援Python的協程。在Werkzeug庫的local模組中,Local
類實現了一種資料結構,用來儲存執行緒的私有變數,對於其具體形式,可以參考它的建構函式:
1 2 3 4 5 |
class Local(object): __slots__ = ('__storage__', '__ident_func__') def __init__(self): object.__setattr__(self, '__storage__', {}) object.__setattr__(self, '__ident_func__', get_ident) |
從上面類定義可以看出,Local
類具有兩個屬性:__storage__
和__ident_func__
。從建構函式來看,__storage__
是一個字典,而__ident_func__
是一個函式,用來識別當前執行緒或協程。
1. __ident_func__
關於當前執行緒或協程的識別,local
模組引入get_ident
函式。如果支援協程,則從greenlet
庫中匯入相關函式,否則從thread
庫中匯入相關函式。呼叫get_ident
將返回一個整數,這個整數可以確定當前執行緒或者協程。
1 2 3 4 5 6 7 |
try: from greenlet import getcurrent as get_ident except ImportError: try: from thread import get_ident except ImportError: from _thread import get_ident |
2. __storage__
__storage__
是一個字典,用來儲存不同的執行緒/協程,以及這些執行緒/協程中的變數。以下是一個簡單的多執行緒的例子,用來說明__storage__
的具體結構。
1 2 3 4 5 6 7 8 9 10 11 |
import threading from werkzeug.local import Local l = Local() l.__storage__ def add_arg(arg, i): l.__setattr__(arg, i) for i in range(3): arg = 'arg' + str(i) t = threading.Thread(target=add_arg, args=(arg, i)) t.start() l.__storage__ |
上面的例子,具體分析為:
- 首先,程式碼建立了一個
Local
的例項l
,並且訪問它的__storage__
屬性。由於目前還沒有資料,所以l.__storage__
的結果為{}
; - 程式碼建立了3個執行緒,每個執行緒均執行
add_arg(arg, i)
函式。這個函式會為每個執行緒建立一個變數,並對其賦值; - 最後,再次訪問
l.__storage__
。這次,l
例項中將包含3個執行緒的資訊。其結果為:
1{20212: {'arg0': 0}, 20404: {'arg1': 1}, 21512: {'arg2': 2}}
從以上結果可以看出,__storage__
這個字典的鍵表示不同的執行緒(通過get_ident
函式獲得執行緒標識數值),而值表示對應執行緒中的變數。這種結構將不同的執行緒分離開來。當某個執行緒要訪問該執行緒的變數時,便可以通過get_ident
函式獲得執行緒標識數值,進而可以在字典中獲得該鍵對應的值資訊了。
三、LocalStack類
LocalStack
類和Local
類類似,但是它實現了棧資料結構。
在LocalStack
類初始化的時候,便會建立一個Local
例項,這個例項用於儲存執行緒/協程的變數。與此同時,LocalStack
類還實現了push
、pop
、top
等方法或屬性。呼叫這些屬性或者方法時,該類會根據當前執行緒或協程的標識數值,在Local
例項中對相應的數值進行操作。以下還是以一個多執行緒的例子進行說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
from werkzeug.local import LocalStack, LocalProxy import logging, random, threading, time # 定義logging配置 logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s', ) # 生成一個LocalStack例項_stack _stack = LocalStack() # 定義一個RequestConetxt類,它包含一個上下文環境。 # 當呼叫這個類的例項時,它會將這個上下文物件放入 # _stack棧中去。當退出該上下文環境時,棧會pop其中 # 的上下文物件。 class RequestConetxt(object): def __init__(self, a, b, c): self.a = a self.b = b self.c = c def __enter__(self): _stack.push(self) def __exit__(self, exc_type, exc_val, exc_tb): if exc_tb is None: _stack.pop() def __repr__(self): return '%s, %s, %s' % (self.a, self.b, self.c) # 定義一個可供不同執行緒呼叫的方法。當不同執行緒呼叫該 # 方法時,首先會生成一個RequestConetxt例項,並在這 # 個上下文環境中先將該執行緒休眠一定時間,之後列印出 # 目前_stack中的資訊,以及當前執行緒中的變數資訊。 # 以上過程會迴圈兩次。 def worker(i): with request_context(i): for j in range(2): pause = random.random() logging.debug('Sleeping %0.02f', pause) time.sleep(pause) logging.debug('stack: %s' % _stack._local.__storage__.items()) logging.debug('ident_func(): %d' % _stack.__ident_func__()) logging.debug('a=%s; b=%s; c=%s' % (LocalProxy(lambda: _stack.top.a), LocalProxy(lambda: _stack.top.b), LocalProxy(lambda: _stack.top.c))) logging.debug('Done') # 呼叫該函式生成一個RequestConetxt物件 def request_context(i): i = str(i+1) return RequestConetxt('a'+i, 'b'+i, 'c'+i) # 在程式最開始顯示_stack的最初狀態 logging.debug('Stack Initial State: %s' % _stack._local.__storage__.items()) # 產生兩個執行緒,分別呼叫worker函式 for i in range(2): t = threading.Thread(target=worker, args=(i,)) t.start() main_thread = threading.currentThread() for t in threading.enumerate(): if t is not main_thread: t.join() # 在程式最後顯示_stack的最終狀態 logging.debug('Stack Finally State: %s' % _stack._local.__storage__.items()) |
以上例子的具體分析過程如下:
- 首先,先建立一個
LocalStack
例項_stack
,這個例項將儲存執行緒/協程的變數資訊; - 在程式開始執行時,先檢查
_stack
中包含的資訊; - 之後建立兩個執行緒,分別執行
worker
函式; worker
函式首先會產生一個上下文物件,這個上下文物件會放入_stack
中。在這個上下文環境中,程式執行一些操作,列印一些資料。當退出上下文環境時,_stack
會pop該上下文物件。- 在程式結束時,再次檢查
_stack
中包含的資訊。
執行上面的測試例子,產生結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
(MainThread) Stack Initial State: [] (Thread-1 ) Sleeping 0.31 (Thread-2 ) Sleeping 0.02 (Thread-2 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})] (Thread-2 ) ident_func(): 13232 (Thread-2 ) a=a2; b=b2; c=c2 (Thread-2 ) Sleeping 0.49 (Thread-1 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})] (Thread-1 ) ident_func(): 880 (Thread-1 ) a=a1; b=b1; c=c1 (Thread-1 ) Sleeping 0.27 (Thread-2 ) stack: [(880, {'stack': [a1, b1, c1]}), (13232, {'stack': [a2, b2, c2]})] (Thread-2 ) ident_func(): 13232 (Thread-2 ) a=a2; b=b2; c=c2 (Thread-2 ) Done (Thread-1 ) stack: [(880, {'stack': [a1, b1, c1]})] (Thread-1 ) ident_func(): 880 (Thread-1 ) a=a1; b=b1; c=c1 (Thread-1 ) Done (MainThread) Stack Finally State: [] |
注意到:
- 當兩個執行緒在執行時,
_stack
中會儲存這兩個執行緒的資訊,每個執行緒的資訊都儲存在類似{'stack': [a1, b1, c1]}
的結構中(注:stack鍵對應的是放入該棧中的物件,此處為了方便列印了該物件的一些屬性)。 - 當執行緒在休眠和執行中切換時,通過執行緒的標識數值進行區分不同執行緒,執行緒1執行時它通過標識數值只會對屬於該執行緒的數值進行操作,而不會和執行緒2的數值混淆,這樣便起到執行緒隔離的效果(而不是通過鎖的方式)。
- 由於是在一個上下文環境中執行,當執行緒執行完畢時,
_stack
會將該執行緒儲存的資訊刪除掉。在上面的執行結果中可以看出,當執行緒2執行結束後,_stack
中只包含執行緒1的相關資訊。當所有執行緒都執行結束,_stack
的最終狀態將為空。