flask之分析線程和協程

flask之分析線程和協程

01 思考:每一個請求之間的關係

咱們每個請求進來的時候都開一個進程確定不合理,那麼若是每個請求進來都是串行的,那麼根本實現不了併發,因此咱們假定每個請求進來使用的是線程。html

那麼線程中數據互相不隔離,存在修改數據的時候數據不安全的問題。python

假定咱們的需求是,每一個線程都要設置值,而且該線程打印該線程修改的值。flask

from threading import Thread,current_thread
import time

class Foo(object):
    def __init__(self):
        self.name = 0

locals_values = Foo()

def func(num):
    locals_values.name = num
    time.sleep(2)             # 取出該線程的名字
    print(locals_values.name, current_thread().name)

for i in range(10):
                                    # 設置該線程的名字
    t = Thread(target=func,args=(i,),name='線程%s'%i)
    t.start()

很明顯阻塞了2秒的時間全部的線程都完成了修改值,而2秒後全部的線程打印出來的時候都是9了,就產生了數據不安全的問題。api

1552975111079

因此咱們要解決這種線程不安全的問題,有以下兩種解決方案。安全

  • 方案一:是加鎖多線程

  • 方案二:使用threading.local對象把要修改的數據複製一份,使得每一個數據互不影響。併發

    咱們要實現的併發是多個請求實現併發,而不是純粹的只是修改一個數據,因此第二種思路更適合作咱們每一個請求的併發,把每一個請求對象的內容都複製一份讓其互相不影響。框架

    詳解:爲何不用加鎖的思路?加鎖的思路是多個線程要真正實現共用一個數據,而且該線程修改了數據以後會影響到其餘線程,更適合相似於12306搶票的應用場景,而咱們是要作請求對象的併發,想要實現的是該線程對於請求對象這部份內容有任何修改並不影響其餘線程。因此使用方案二ide

02 threading.local

多個線程修改同一個數據,複製多份數據給每一個線程用,爲每一個線程開闢一塊空間進行數據存儲性能

實例:

from threading import Thread,current_thread,local
import time

locals_values = local()
# 能夠簡單理解爲,識別到新的線程的時候,都會開闢一片新的內存空間,至關於每一個線程對該值進行了拷貝。

def func(num):
    locals_values.name = num
    time.sleep(2)
    print(locals_values.name, current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='線程%s'%i)
    t.start()

1552976228090

如上經過threading.local實例化的對象,實現了多線程修改同一個數據,每一個線程都複製了一份數據,而且修改的也都是本身的數據。達到了咱們想要的效果。

03 經過字典自定義threading.local

實例:

from threading import get_ident,Thread,current_thread
# get_ident()能夠獲取每一個線程的惟一標記,
import time

class Local(object):
    storage = {}# 初始化一個字典
    get_ident = get_ident # 拿到get_ident的地址
    def set(self,k,v):
        ident =self.get_ident()# 獲取當前線程的惟一標記
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def get(self,k):
        ident = self.get_ident() # 獲取當前線程的惟一標記
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
def func(num):
    # get_ident() 獲取當前線程的惟一標記
    locals_values.set('KEY',num)
    time.sleep(2)
    print(locals_values.get('KEY'),current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='線程%s'%i)
    t.start()

講解:

利用get_ident()獲取每一個線程的惟一標記做爲鍵,而後組織一個字典storage。

:{線程1的惟一標記:{k:v},線程2的惟一標記:{k:v}.......}

{
    15088: {'KEY': 0}, 
    8856: {'KEY': 1},
    17052: {'KEY': 2}, 
    8836: {'KEY': 3}, 
    13832: {'KEY': 4}, 
    15504: {'KEY': 5}, 
    16588: {'KEY': 6}, 
    5164: {'KEY': 7}, 
    560: {'KEY': 8}, 
    1812: {'KEY': 9}
                    }

運行效果

1552981453617

04 經過setattr和getattr實現自定義threthreading.local

實例

from threading import get_ident,Thread,current_thread
# get_ident()能夠獲取每一個線程的惟一標記,
import time

class Local(object):
    storage = {}# 初始化一個字典
    get_ident = get_ident # 拿到get_ident的地址

    def __setattr__(self, k, v):
        ident =self.get_ident()# 獲取當前線程的惟一標記
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def __getattr__(self, k):
        ident = self.get_ident() # 獲取當前線程的惟一標記
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
def func(num):
    # get_ident() 獲取當前線程的惟一標記
    locals_values.KEY=num
    time.sleep(2)
    print(locals_values.KEY,current_thread().name)

for i in range(10):
    t = Thread(target=func,args=(i,),name='線程%s'%i)
    t.start()

05 每一個對象有本身的存儲空間(字典)

咱們能夠自定義實現了threading.local的功能,可是如今存在一個問題,若是咱們想生成多個Local對象,可是會致使多個Local對象所管理的線程設置的內容都放到了類屬性storage = {}裏面,因此咱們若是想實現每個Local對象所對應的線程設置的內容都放到本身的storage裏面,就須要從新設計代碼。

實例:

from threading import get_ident,Thread,current_thread
# get_ident()能夠獲取每一個線程的惟一標記,
import time

class Local(object):
    def __init__(self):
        # 千萬不要按照註釋裏這麼寫,不然會形成遞歸死循環,死循環在__getattr__中,不理解的話能夠全程使用debug測試。
        # self.storage = {}
        # self.get_ident =get_ident
        object.__setattr__(self,"storage",{})
        object.__setattr__(self,"get_ident",get_ident) # 借用父類設置對象的屬性,避免遞歸死循環。

    def __setattr__(self, k, v):
        ident =self.get_ident() # 獲取當前線程的惟一標記
        origin = self.storage.get(ident)
        if not origin:
            origin={}
        origin[k] = v
        self.storage[ident] = origin
    def __getattr__(self, k):
        ident = self.get_ident() # 獲取當前線程的惟一標記
        v= self.storage[ident].get(k)
        return v

locals_values = Local()
locals_values2 = Local()
def func(num):
    # get_ident() 獲取當前線程的惟一標記
    # locals_values.set('KEY',num)
    locals_values.KEY=num
    time.sleep(2)
    print(locals_values.KEY,current_thread().name)
    # print('locals_values2.storage:',locals_values2.storage) # 查看locals_values2.storage的私有的storage

for i in range(10):
    t = Thread(target=func,args=(i,),name='線程%s'%i)
    t.start()

顯示效果我就不作演示了,和前幾個案例演示效果同樣。

06 若是是你會如何設計flask的請求併發?

  • 狀況一:單進程單線程,基於全局變量就能夠作

  • 狀況二:單進程多線程,基於threading.local對象作

  • 狀況三:單進程多線程多協程,如何作?

    提示:協程屬於應用級別的,協程會替代操做系統自動切換遇到 IO的任務或者運行級別低的任務,而應用級別的切換速度遠高於操做系統的切換

    固然若是是本身來設計框架,爲了提高程序的併發性能,必定是上訴的狀況三,不光考慮多線程而且要多協程,那麼該如何設計呢?

    在咱們的flask中爲了這種併發需求,依賴於底層的werkzeug外部包,werkzeug實現了保證多線程和多協程的安全,werkzeug基本的設計理念和上一個案例一致,惟一的區別就是在導入的時候作了一步處理,且看werkzeug源碼。

    werkzeug.local.py部分源碼

    ...
    
    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):
        ...
    
        def __init__(self):
            object.__setattr__(self, '__storage__', {})
            object.__setattr__(self, '__ident_func__', get_ident)
    
          ...
    
        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}

    分析:

    原理就是在最開始導入線程和協程的惟一標識的時候統一命名爲get_ident,而且先導入協程模塊的時候若是報錯說明不支持協程,就會去導入線程的get_ident,這樣不管是隻有線程運行仍是協程運行均可以獲取惟一標識,而且把這個標識的線程或協程須要設置的內容都分類存放於__storage__字典中。

原文出處:https://www.cnblogs.com/guapitomjoy/p/12061463.html

相關文章
相關標籤/搜索