Flask中請求資料的優雅傳遞

金色旭光發表於2022-02-17

當一個請求到來時,瀏覽器會攜帶很多資訊傳送傳送服務端。在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"}}。

相關文章