Python技術分享:深入理解ThreadLocal變數的功能和使用

千鋒武漢發表於2021-04-28

      我們在進行Python多執行緒開發的時候經常會使用到變數,但全域性變數的變化影響到每一個執行緒,而區域性變數使用起來又非常麻煩,所以我們需要使用到ThreadLocal變數,下面小千就來給大家介紹這個ThreadLocal變數到底是什麼怎麼用?

      ThreadLocal變數它本身是一個全域性變數,但是每個執行緒卻可以利用它來儲存屬於自己的私有資料,這些私有資料對其他執行緒也是不可見的。下圖給出了執行緒中這幾種變數的存在情況

1

      全域性 VS 區域性變數

      首先借助一個小程式來看看多執行緒環境下全域性變數的同步問題。

2

      這裡我們建立了10個執行緒,每個執行緒均對全域性變數 global_num 進行1000次的加1操作(迴圈1000次加1是為了延長單個執行緒執行時間,使執行緒執行時被中斷切換),當10個執行緒執行完畢時,全域性變數的值是多少呢?

      答案是不確定,簡單來說是因為 global_num += 1 並不是一個原子操作,因此執行過程可能被其他執行緒中斷,導致其他執行緒讀到一個髒值。以兩個執行緒執行 +1 為例,其中一個可能的執行序列如下(此情況下最後結果為1)

      多執行緒中使用全域性變數時普遍存在這個問題,解決辦法也很簡單,可以使用互斥鎖、條件變數或者是讀寫鎖。下面考慮用互斥鎖來解決上面程式碼的問題,只需要在進行 +1 運算前加鎖,運算完畢釋放鎖即可,這樣就可以保證運算的原子性。

3

      線上程中使用區域性變數則不存在這個問題,因為每個執行緒的區域性變數不能被其他執行緒訪問。下面我們用10個執行緒分別對各自的區域性變數進行1000次加1操作,每個執行緒結束時列印一共執行的操作次數(每個執行緒均為1000)

Python變數

      可以看出這裡每個執行緒都有自己的 local_num,各個執行緒之間互不干涉。

threadlocal

      Thread-local 物件

      上面程式中我們需要給 show 函式傳遞 local_num 區域性變數,並沒有什麼不妥。不過考慮在實際生產環境中,我們可能會呼叫很多函式,每個函式都需要很多區域性變數,這時候用傳遞引數的方法會很不友好。

      為了解決這個問題,一個直觀的的方法就是建立一個全域性字典,儲存程式 ID 到該程式區域性變數的對映關係,執行中的執行緒可以根據自己的 ID 來獲取本身擁有的資料。這樣,就可以避免在函式呼叫中傳遞引數,如下示例:

Python變數2

      儲存一個全域性字典,然後將執行緒識別符號作為key,相應執行緒的區域性資料作為 value,這種做法並不完美。

      首先,每個函式在需要執行緒區域性資料時,都需要先取得自己的執行緒ID,略顯繁瑣。更糟糕的是,這裡並沒有真正做到執行緒之間資料的隔離,因為每個執行緒都可以讀取到全域性的字典,每個執行緒都可以對字典內容進行更改。

      為了更好解決這個問題,python 執行緒庫實現了 ThreadLocal 變數(很多語言都有類似的實現,比如Java)。ThreadLocal 真正做到了執行緒之間的資料隔離,並且使用時不需要手動獲取自己的執行緒 ID,如下示例

Python變數3

      上面示例中每個執行緒都可以透過 global_data.num 獲得自己獨有的資料,並且每個執行緒讀取到的 global_data 都不同,真正做到執行緒之間的隔離。

      ThreadLocal 實現的程式碼量不多,但是比較難理解,涉及很多 Python 黑魔法,下篇再來分析。那麼 ThreadLocal 很完美了?不!Python 的 WSGI 工具庫 werkzeug 中有一個更好的 ThreadLocal 實現,甚至支援協程之間的私有資料,實現更加複雜,有機會再分析。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31548651/viewspace-2770226/,如需轉載,請註明出處,否則將追究法律責任。

相關文章