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"}}。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章