當一個請求到來時,瀏覽器會攜帶很多資訊傳送傳送服務端。在Django中,每一個處理函式都要傳入一個request的引數,該引數攜帶所有請求的資訊,也就是服務端程式封裝的environ
(不明白該引數可以參見上一篇flask初探之WSGI)。簡單示例如下
from django.shortcuts import render
def index(request):
context = {}
return render(request, "index.html", context)
每一個請求攜帶的資料都可以從request傳入到處理函式中,這種處理方法可以稱之為顯示傳遞。
接收請求資料在Flask中有一種更巧妙的實現:當有請求到來時request就會變成一個全域性變數,所有的處理函式可以直接使用request這個全域性變數,而不需要顯示傳入引數。簡單示例如下:
import time
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello '+request.args.get("name")
這種設計減少了每個函式需要傳入的引數,比起Django的顯示傳參更加優雅。
但是這種全域性變數也會自己的問題,多執行緒的情況下同一時間能夠處理多個請求,每個處理函式都需要自己的請求資訊,如何保證處理函式和請求一一對應呢?Flask主要使用本地執行緒技術來保證請求資訊和處理函式相互的對應。下面主要介紹本地執行緒技術。
本地執行緒
在多執行緒程式設計中,全域性變數不可避免的會競爭,通常使用加鎖來解決競爭。此外有一種本地執行緒
技術可以讓每一個執行緒都擁有自己的私有的變數。比如全域性變數a,使用本地執行緒技術可以讓每一個執行緒對a處理時都是互相隔離的,彼此之間不影響。下面從區域性變數、全域性變數和本地執行緒三個例子對比說明本地執行緒技術。
區域性變數
開啟多執行緒,每個子執行緒完成不同的計算任務,x是執行緒中的區域性變數。
每個子執行緒都有獨立的空間。每次壓棧,區域性變數x的作用域地址是不同的(執行緒獨享),計算結果互不干擾。
import time
import threading
def worker():
x = 0
for i in range(100):
time.sleep(0.0001)
x += 1
print(threading.current_thread(),x)
for i in range(10):
threading.Thread(target=worker).start()
執行結果:
<Thread(Thread-2, started 123145372971008)> 100
<Thread(Thread-6, started 123145393991680)> 100
<Thread(Thread-1, started 123145367715840)> 100
<Thread(Thread-3, started 123145378226176)> 100
<Thread(Thread-5, started 123145388736512)> 100
<Thread(Thread-7, started 123145399246848)> 100
<Thread(Thread-4, started 123145383481344)> 100
<Thread(Thread-10, started 123145415012352)> 100
<Thread(Thread-8, started 123145404502016)> 100
<Thread(Thread-9, started 123145409757184)> 100
全域性變數
當多執行緒使用全域性變數時就會發生搶佔和競爭
import threading
import time
x = 0
def worker():
global x
x = 0
for i in range(100):
time.sleep(0.0001)
x += 1
print(threading.current_thread(),x)
for i in range(10):
threading.Thread(target=worker).start()
執行結果:
<Thread(Thread-2, started 123145483571200)> 888
<Thread(Thread-5, started 123145499336704)> 908
<Thread(Thread-3, started 123145488826368)> 930
<Thread(Thread-4, started 123145494081536)> 937
<Thread(Thread-1, started 123145478316032)> 941
<Thread(Thread-6, started 123145504591872)> 947
<Thread(Thread-7, started 123145509847040)> 949
<Thread(Thread-8, started 123145515102208)> 955
<Thread(Thread-9, started 123145520357376)> 962
<Thread(Thread-10, started 123145525612544)> 964
希望的結果是100,最後卻遠大於100。原因在於第一個執行緒將全域性變數+1之後,第二個執行緒在這個基礎上繼續+1,第三個執行緒在繼續對x+1,每個執行緒都對全域性變數+1,最終結果就不符合預期。
本地執行緒
本地執行緒可以避免上面全域性變數競爭問題。標準庫threading
中就自帶本地執行緒物件。
import time
import threading
a = threading.local() # 全域性物件
def worker():
a.x = 0
for i in range(100):
time.sleep(0.0001)
a.x += 1
print(threading.current_thread(),a.x)
for i in range(10):
threading.Thread(target=worker).start()
執行結果:
<Thread(Thread-4, started 123145570172928)> 100
<Thread(Thread-6, started 123145580683264)> 100
<Thread(Thread-1, started 123145554407424)> 100
<Thread(Thread-2, started 123145559662592)> 100
<Thread(Thread-8, started 123145591193600)> 100
<Thread(Thread-5, started 123145575428096)> 100
<Thread(Thread-3, started 123145564917760)> 100
<Thread(Thread-7, started 123145585938432)> 100
<Thread(Thread-10, started 123145601703936)> 100
<Thread(Thread-9, started 123145596448768)> 100
本質上本地執行緒物件就是一個字典的子類,為每一個執行緒建立一個鍵值對,key是執行緒id,value是值。當某一個執行緒操作變數時就是操作自己的id物件的值。
如上例中本地執行緒是a,可將其看做一個字典a = {"執行緒id": x}。執行緒1中a={"123145570172928":44},執行緒2中a={"123145559662592": 55}。所以各個執行緒之間雖然引用了同名變數,但實際上是互相不干擾的。
LocalStack
本地棧和本地執行緒類似的功能,本地執行緒常用來處理數字或字串等簡單資料結構,維護了{"執行緒id":值}這樣一個關係。本地棧是一個可以當做棧來使用的結構,本質上也是一個字典,結構為{"執行緒id":{"stack":[]}。這個資料結構的主要是能夠使用壓棧和出棧等操作,方便先進後出的場景。
簡單使用
import time
from werkzeug.local import LocalStack
local_stack = LocalStack()
local_stack.push("abc")
local_stack.push("xyz")
# 獲取棧頂元素,不彈出元素
print(local_stack.top)
# 彈出棧頂元素,出棧
print(local_stack.pop())
# 再次獲取棧頂,棧頂元素已變化
print(local_stack.top)
執行結果:
xyz
xyz
abc
執行緒互不干擾
import threading
from werkzeug.local import LocalStack
def worker(local_stack):
print(local_stack.top) # 主執行緒中壓棧了資料,但是在子線執行緒中取不到,執行緒互相隔離。
if __name__ == "__main__":
local_stack = LocalStack()
local_stack.push("主執行緒")
threading.Thread(target=worker, args=(local_stack,)).start()
print(local_stack.top)
執行結果:
None
主執行緒
request的執行緒隔離實現
通過本地執行緒技術,request雖然是全域性變數,但是在每一個執行緒中都是互相隔離的。
但需要說明的是Flask中並不是使用標準執行緒庫的本地執行緒物件,因為還需要相容協程,所以flask使用了werkzeug中的本地執行緒物件werkzeug.local.Local()
。werkzeug的本地執行緒物件增加了對Greenlet的優先支援。
werkzeug中本地執行緒的實現
# since each thread has its own greenlet we can just use those as identifiers
# for the context. If greenlets are not available we fall back to the
# current thread ident depending on where it is.
try:
from greenlet import getcurrent as get_ident
except ImportError:
try:
from thread import get_ident
except ImportError:
from _thread import get_ident
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):
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)
從import可以看出,首先是從協程匯入,如果報錯再從執行緒匯入。在__setattr__
函式新增變數時,首先是通過get_ident方法獲取了執行緒id,然後將執行緒id作為key,value又是一個字典{name:value}。類似於{"執行緒id":{"name": "value"}}。