鍍金池/ 教程/ Python/ ThreadLocal
基礎(chǔ)
itertools
HTTP 服務(wù)
hashlib
閉包
文件和目錄
類(lèi)
單元測(cè)試
使用 @property
標(biāo)準(zhǔn)模塊
陌生的 metaclass
Base64
進(jìn)程、線(xiàn)程和協(xié)程
讀寫(xiě)二進(jìn)制文件
匿名函數(shù)
輸入和輸出
Click
元組
字符編碼
partial 函數(shù)
參考資料
collections
協(xié)程
類(lèi)和實(shí)例
Python 之旅
定制類(lèi)和魔法方法
常用數(shù)據(jù)類(lèi)型
繼承和多態(tài)
ThreadLocal
HTTP 協(xié)議簡(jiǎn)介
Requests 庫(kù)的使用
讀寫(xiě)文本文件
列表
os 模塊
迭代器 (Iterator)
正則表達(dá)式
集合
上下文管理器
異常處理
你不知道的 super
定義函數(shù)
datetime
資源推薦
字典
slots 魔法
hmac
第三方模塊
進(jìn)程
類(lèi)方法和靜態(tài)方法
函數(shù)參數(shù)
高階函數(shù)
函數(shù)
re 模塊
高級(jí)特性
線(xiàn)程
argparse
生成器
結(jié)束語(yǔ)
字符串
map/reduce/filter
函數(shù)式編程
Celery
裝飾器

ThreadLocal

我們知道,同一進(jìn)程的多個(gè)線(xiàn)程之間是內(nèi)存共享的,這意味著,當(dāng)一個(gè)線(xiàn)程對(duì)全局變量做了修改,將會(huì)影響到其他所有線(xiàn)程,這是很危險(xiǎn)的。為了避免多個(gè)線(xiàn)程同時(shí)修改全局變量,我們就需要對(duì)全局變量的修改加鎖。

除了對(duì)全局變量的修改進(jìn)行加鎖,你可能也想到了可以使用線(xiàn)程自己的局部變量,因?yàn)榫植孔兞恐挥芯€(xiàn)程自己能看見(jiàn),對(duì)同一進(jìn)程的其他線(xiàn)程是不可訪(fǎng)問(wèn)的。確實(shí)如此,讓我們先看一個(gè)例子:

from threading import Thread, current_thread

def echo(num):
    print current_thread().name, num

def calc():
    print 'thread %s is running...' % current_thread().name
    local_num = 0
    for _ in xrange(10000):
        local_num += 1
    echo(local_num)
    print 'thread %s ended.' % current_thread().name

if __name__ == '__main__':
    print 'thread %s is running...' % current_thread().name

    threads = []
    for i in range(5):
        threads.append(Thread(target=calc))
        threads[i].start()
    for i in range(5):
        threads[i].join()

    print 'thread %s ended.' % current_thread().name

在上面的代碼中,我們創(chuàng)建了 5 個(gè)線(xiàn)程,每個(gè)線(xiàn)程都對(duì)自己的局部變量 local_num 進(jìn)行 10000 次的加 1 操作。由于對(duì)線(xiàn)程局部變量的修改不會(huì)影響到其他線(xiàn)程,因此,我們可以看到,每個(gè)線(xiàn)程結(jié)束時(shí)打印的 local_num 的值都為 10000,執(zhí)行結(jié)果如下:

thread MainThread is running...
thread Thread-4 is running...
Thread-4 10000
thread Thread-4 ended.
thread Thread-5 is running...
Thread-5 10000
thread Thread-5 ended.
thread Thread-6 is running...
Thread-6 10000
thread Thread-6 ended.
thread Thread-7 is running...
Thread-7 10000
thread Thread-7 ended.
thread Thread-8 is running...
Thread-8 10000
thread Thread-8 ended.
thread MainThread ended.

上面這種線(xiàn)程使用自己的局部變量的方法雖然可以避免多線(xiàn)程對(duì)同一變量的訪(fǎng)問(wèn)沖突,但還是有一些問(wèn)題。在實(shí)際的開(kāi)發(fā)中,我們會(huì)調(diào)用很多函數(shù),每個(gè)函數(shù)又有很多個(gè)局部變量,這時(shí)每個(gè)函數(shù)都這么傳參數(shù)顯然是不可取的。

為了解決這個(gè)問(wèn)題,一個(gè)比較容易想到的做法就是創(chuàng)建一個(gè)全局字典,以線(xiàn)程的 ID 作為 key,線(xiàn)程的局部數(shù)據(jù)作為 value,這樣就可以消除函數(shù)傳參的問(wèn)題,代碼如下:

from threading import Thread, current_thread

global_dict = {}

def echo():
    num = global_dict[current_thread()]    # 線(xiàn)程根據(jù)自己的 ID 獲取數(shù)據(jù)
    print current_thread().name, num

def calc():
    print 'thread %s is running...' % current_thread().name

    global_dict[current_thread()] = 0
    for _ in xrange(10000):
        global_dict[current_thread()] += 1
    echo()

    print 'thread %s ended.' % current_thread().name

if __name__ == '__main__':
    print 'thread %s is running...' % current_thread().name

    threads = []
    for i in range(5):
        threads.append(Thread(target=calc))
        threads[i].start()
    for i in range(5):
        threads[i].join()

    print 'thread %s ended.' % current_thread().name

看下執(zhí)行結(jié)果:

thread MainThread is running...
thread Thread-64 is running...
thread Thread-65 is running...
thread Thread-66 is running...
thread Thread-67 is running...
thread Thread-68 is running...
Thread-67 10000
thread Thread-67 ended.
Thread-65 10000
thread Thread-65 ended.
Thread-68 10000
thread Thread-68 ended.
Thread-66 10000
thread Thread-66 ended.
Thread-64 10000
thread Thread-64 ended.
thread MainThread ended.

上面的做法雖然消除了函數(shù)傳參的問(wèn)題,但是還是有些不完美,為了獲取線(xiàn)程的局部數(shù)據(jù),我們需要先獲取線(xiàn)程 ID,另外,global_dict 是個(gè)全局變量,所有線(xiàn)程都可以對(duì)它進(jìn)行修改,還是有些危險(xiǎn)。

那到底如何是好?

事實(shí)上,Python 提供了 ThreadLocal 對(duì)象,它真正做到了線(xiàn)程之間的數(shù)據(jù)隔離,而且不用查找 dict,代碼如下:

from threading import Thread, current_thread, local

global_data = local()

def echo():
    num = global_data.num
    print current_thread().name, num

def calc():
    print 'thread %s is running...' % current_thread().name

    global_data.num = 0
    for _ in xrange(10000):
        global_data.num += 1
    echo()

    print 'thread %s ended.' % current_thread().name

if __name__ == '__main__':
    print 'thread %s is running...' % current_thread().name

    threads = []
    for i in range(5):
        threads.append(Thread(target=calc))
        threads[i].start()
    for i in range(5):
        threads[i].join()

    print 'thread %s ended.' % current_thread().name

在上面的代碼中,global_data 就是 ThreadLocal 對(duì)象,你可以把它當(dāng)作一個(gè)全局變量,但它的每個(gè)屬性,比如 global_data.num 都是線(xiàn)程的局部變量,沒(méi)有訪(fǎng)問(wèn)沖突的問(wèn)題。

讓我們看下執(zhí)行結(jié)果:

thread MainThread is running...
thread Thread-94 is running...
thread Thread-95 is running...
thread Thread-96 is running...
thread Thread-97 is running...
thread Thread-98 is running...
Thread-96 10000
thread Thread-96 ended.
Thread-97 10000
thread Thread-97 ended.
Thread-95 10000
thread Thread-95 ended.
Thread-98 10000
thread Thread-98 ended.
Thread-94 10000
thread Thread-94 ended.
thread MainThread ended.

小結(jié)

  • 使用 ThreadLocal 對(duì)象來(lái)線(xiàn)程綁定自己獨(dú)有的數(shù)據(jù)。

參考資料