Flask中請求數據的優雅傳遞
當一個請求到來時,瀏覽器會攜帶很多信息發送發送服務端。在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”}}。