Python中的ThreadLocal變量

我們知道多線程環境下,每一個線程均可以使用所屬進程的全局變量。如果一個線程對全局變量進行了修改,將會影響到其他所有的線程。爲了避免多個線程同時對變量進行修改,引入了線程同步機制,通過互斥鎖,條件變量或者讀寫鎖來控制對全局變量的訪問。

只用全局變量並不能滿足多線程環境的需求,很多時候線程還需要擁有自己的私有數據,這些數據對於其他線程來說不可見。因此線程中也可以使用局部變量,局部變量只有線程自身可以訪問,同一個進程下的其他線程不可訪問。

有時候使用局部變量不太方便,因此 python 還提供了 ThreadLocal 變量,它本身是一個全局變量,但是每個線程卻可以利用它來保存屬於自己的私有數據,這些私有數據對其他線程也是不可見的。

全局 VS 局部變量

首先借助一個小程序來看看多線程環境下全局變量的同步問題。

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 運算前加鎖,運算完畢釋放鎖即可,這樣就可以保證運算的原子性。

l = threading.Lock()
...
    l.acquire()
    global_num += 1
    l.release()

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

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,各個線程之間互不干涉。

 

Thread-local 對象

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

爲了解決這個問題,一個直觀的的方法就是建立一個全局字典,保存進程 ID 到該進程局部變量的映射關係,運行中的線程可以根據自己的 ID 來獲取本身擁有的數據。這樣,就可以避免在函數調用中傳遞參數,如下示例:

global_data = {}
def show():
    cur_thread = threading.current_thread()
    print cur_thread.getName(), global_data[cur_thread]
def thread_cal():
    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__ # {}

 

 

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