Werkzeug 庫之 local 模塊
點擊
上方藍字
,快速關注我們)
來源:fanchunke
fanchunke.me/Flask/Werkzeug庫——local模塊/
如有好文章投稿,請點擊 → 這裡了解詳情
一、簡介
在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類實現了一種數據結構,用來保存線程的私有變數,對於其具體形式,可以參考它的構造函數:
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將返回一個整數,這個整數可以確定當前線程或者協程。
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__的具體結構。
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個線程的信息。其結果為:
{20212: {"arg0": 0}, 20404: {"arg1": 1}, 21512: {"arg2": 2}}
從以上結果可以看出,__storage__這個字典的鍵表示不同的線程(通過get_ident函數獲得線程標識數值),而值表示對應線程中的變數。這種結構將不同的線程分離開來。當某個線程要訪問該線程的變數時,便可以通過get_ident函數獲得線程標識數值,進而可以在字典中獲得該鍵對應的值信息了。
三、LocalStack類
LocalStack類和Local類類似,但是它實現了棧數據結構。
在LocalStack類初始化的時候,便會創建一個Local實例,這個實例用於存儲線程/協程的變數。與此同時,LocalStack類還實現了push、pop、top等方法或屬性。調用這些屬性或者方法時,該類會根據當前線程或協程的標識數值,在Local實例中對相應的數值進行操作。以下還是以一個多線程的例子進行說明:
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中包含的信息。
運行上面的測試例子,產生結果如下:
(
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的最終狀態將為空。
看完本文有收穫?請轉
發分享給更多人
關注「Python開發者」,提升Python技能
※PyPy v5.8 發布,有這些新亮點
※量化投資重磅上線,橫掃金融界就靠你啦!
※Werkzeug 庫以及 wrapper 模塊
※這裡有 10 個省時間的 PyCharm 技巧
TAG:Python開發者 |