咱們知道多線程環境下,每個線程都可以使用所屬進程的全局變量。若是一個線程對全局變量進行了修改,將會影響到其餘全部的線程。爲了不多個線程同時對變量進行修改,引入了線程同步機制,經過互斥鎖,條件變量或者讀寫鎖來控制對全局變量的訪問。html
只用全局變量並不能知足多線程環境的需求,不少時候線程還須要擁有本身的私有數據,這些數據對於其餘線程來講不可見。所以線程中也可使用局部變量,局部變量只有線程自身能夠訪問,同一個進程下的其餘線程不可訪問。python
有時候使用局部變量不太方便,所以 python 還提供了 ThreadLocal 變量,它自己是一個全局變量,可是每一個線程卻能夠利用它來保存屬於本身的私有數據,這些私有數據對其餘線程也是不可見的。下圖給出了線程中這幾種變量的存在狀況:git
首先借助一個小程序來看看多線程環境下全局變量的同步問題。github
import threading global_num = 0 def thread_cal(): global global_num for i in xrange(1000): global_num += 1 # Get 10 threads, run them and wait them all finished. threads = [] for i in range(10): threads.append(threading.Thread(target=thread_cal)) threads[i].start() for i in range(10): threads[i].join() # Value of global variable can be confused. print global_num
這裏咱們建立了10個線程,每一個線程均對全局變量 global_num 進行1000次的加1操做(循環1000次加1是爲了延長單個線程執行時間,使線程執行時被中斷切換),當10個線程執行完畢時,全局變量的值是多少呢?答案是不肯定。簡單來講是由於 global_num += 1
並非一個原子操做,所以執行過程可能被其餘線程中斷,致使其餘線程讀到一個髒值。以兩個線程執行 +1 爲例,其中一個可能的執行序列以下(此狀況下最後結果爲1):小程序
多線程中使用全局變量時廣泛存在這個問題,解決辦法也很簡單,可使用互斥鎖、條件變量或者是讀寫鎖。下面考慮用互斥鎖來解決上面代碼的問題,只須要在進行 +1 運算前加鎖,運算完畢釋放鎖便可,這樣就能夠保證運算的原子性。多線程
l = threading.Lock() ... l.acquire() global_num += 1 l.release()
在線程中使用局部變量則不存在這個問題,由於每一個線程的局部變量不能被其餘線程訪問。下面咱們用10個線程分別對各自的局部變量進行1000次加1操做,每一個線程結束時打印一共執行的操做次數(每一個線程均爲1000):app
def show(num): print threading.current_thread().getName(), num def thread_cal(): local_num = 0 for _ in xrange(1000): local_num += 1 show(local_num) threads = [] for i in range(10): threads.append(threading.Thread(target=thread_cal)) threads[i].start()
能夠看出這裏每一個線程都有本身的 local_num,各個線程之間互不干涉。函數
上面程序中咱們須要給 show 函數傳遞 local_num 局部變量,並無什麼不妥。不過考慮在實際生產環境中,咱們可能會調用不少函數,每一個函數都須要不少局部變量,這時候用傳遞參數的方法會很不友好。工具
爲了解決這個問題,一個直觀的的方法就是創建一個全局字典,保存進程 ID 到該進程局部變量的映射關係,運行中的線程能夠根據本身的 ID 來獲取自己擁有的數據。這樣,就能夠避免在函數調用中傳遞參數,以下示例:ui
global_data = {} def show(): cur_thread = threading.current_thread() print cur_thread.getName(), global_data[cur_thread] def thread_cal(): global global_data cur_thread = threading.current_thread() global_data[cur_thread] = 0 for _ in xrange(1000): global_data[cur_thread] += 1 show() # Need no local variable. Looks good. ...
保存一個全局字典,而後將線程標識符做爲key,相應線程的局部數據做爲 value,這種作法並不完美。首先,每一個函數在須要線程局部數據時,都須要先取得本身的線程ID,略顯繁瑣。更糟糕的是,這裏並無真正作到線程之間數據的隔離,由於每一個線程均可以讀取到全局的字典,每一個線程均可以對字典內容進行更改。
爲了更好解決這個問題,python 線程庫實現了 ThreadLocal 變量(不少語言都有相似的實現,好比Java)。ThreadLocal 真正作到了線程之間的數據隔離,而且使用時不須要手動獲取本身的線程 ID,以下示例:
global_data = threading.local() def show(): print threading.current_thread().getName(), global_data.num def thread_cal(): global_data.num = 0 for _ in xrange(1000): global_data.num += 1 show() threads = [] ... print "Main thread: ", global_data.__dict__ # {}
上面示例中每一個線程均可以經過 global_data.num 得到本身獨有的數據,而且每一個線程讀取到的 global_data 都不一樣,真正作到線程之間的隔離。
ThreadLocal 實現的代碼量很少,可是比較難理解,涉及不少 Python 黑魔法,下篇再來分析。那麼 ThreadLocal 很完美了?不!Python 的 WSGI 工具庫 werkzeug 中有一個更好的 ThreadLocal 實現,甚至支持協程之間的私有數據,實現更加複雜,有機會再分析。
Thread local storage in Python
threading – Manage concurrent threads
Python線程同步機制
Linux多線程與同步
Are local variables in a python function thread safe?
本文由 selfboot 發表於 我的博客,採用署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議。
非商業轉載請註明做者及出處。商業轉載請聯繫做者本人。
本文標題爲: ThreadLocal之應用篇
本文連接爲: http://selfboot.cn/2016/08/22...