深入理解Python中的ThreadLocal變量(中)

作者:selfboot

在 深入理解Python中的ThreadLocal變量(上) 中我們看到 ThreadLocal 的引入斟珊,使得可以很方便地在多線程環(huán)境中使用局部變量。如此美妙的功能到底是怎樣實現(xiàn)的富纸?如果你對它的實現(xiàn)原理沒有好奇心或一探究竟的沖動囤踩,那么接下來的內(nèi)容估計會讓你后悔自己的淺嘗輒止了旨椒。

簡單來說,Python 中 ThreadLocal 就是通過下圖中的方法堵漱,將全局變量偽裝成線程局部變量综慎,相信讀完本篇文章你會理解圖中內(nèi)容的。(對這張圖不眼熟的話勤庐,可以回顧下上篇))示惊。

在哪里找到源碼涝涤?


好了阔拳,終于要來分析 ThreadLocal 是如何實現(xiàn)的啦糊肠,不過货裹,等等弧圆,怎么找到它的源碼呢笔咽?上一篇中我們只是用過它(from threading import local)叶组,從這里只能看出它是在 threading 模塊實現(xiàn)的甩十,那么如何找到 threading 模塊的源碼呢侣监。

如果你在使用 PyCharm,恭喜你窃爷,你可以用 View source(OS X 快捷鍵是 ?↓)找到 local 定義的地方⊥萄迹現(xiàn)在許多 IDE 都有這個功能刻剥,可以查看 IDE 的幫助來找到該功能造虏。接著我們就會發(fā)現(xiàn) local 是這樣子的(這里以 python 2.7 為例):

# get thread-local implementation, either from the thread
# module, or from the python fallback
try:
    from thread import _local as local
except ImportError:
    from _threading_local import local

嗯麦箍,自帶解釋漓藕,非常好。我們要做的是繼續(xù)往下深挖具體實現(xiàn)挟裂,用同樣的方法(?↓)找 _local 的實現(xiàn)享钞,好像不太妙,沒有找到純 python 實現(xiàn):

class _local(object):
    """ Thread-local data """
    def __delattr__(self, name): # real signature unknown; restored from __doc__
        """ x.__delattr__('name') <==> del x.name """
        pass
    ...

沒關(guān)系诀蓉,繼續(xù)來看下_threading_local吧栗竖,這下子終于找到了local的純 python 實現(xiàn)。開始就是很長的一段注釋文檔渠啤,告訴我們這個模塊是什么狐肢,如何用。這個文檔的質(zhì)量非常高份名,值得我們?nèi)W(xué)習(xí)。所以僵腺,再次后悔自己的淺嘗輒止了吧壶栋,差點錯過了這么優(yōu)秀的文檔范文!

將源碼私有化


在具體動手分析這個模塊之前委刘,我們先把它拷出來放在一個單獨的文件 thread_local.py 中丧没,這樣可以方便我們隨意肢解它(比如在適當(dāng)?shù)牡胤郊由蟣og),并用修改后的實現(xiàn)驗證我們的一些想法锡移。此外呕童,如果你真的理解了_threading_local.py最開始的一段淆珊,你就會發(fā)現(xiàn)這樣做是多么的有必要擂找。因為python的threading.local不一定是用的_threading_local(還記得class _local(object) 嗎慢洋?)。

所以如果你用 threading.local 來驗證自己對_threading_local.py的理解妻顶,你很可能會一頭霧水的醇王。不幸的是,我開始就這樣干的寞埠,所以被下面的代碼坑了好久:

from threading import local, current_thread
data = local()
key = object.__getattribute__(data, '_local__key') 
print current_thread().__dict__.get(key)
# AttributeError: 'thread._local' object has no attribute ‘_local__key'

當(dāng)然,你可能不理解這里是什么意思,沒關(guān)系昌抠,我只是想強調(diào)在 threading.local 沒有用到_threading_local.py,你必須要創(chuàng)建一個模塊(我將它命名為 thread_local.py)來保存_threading_local里面的內(nèi)容,然后像下面這樣驗證自己的想法:

from threading import current_thread
from thread_local import local

data = local()
key = object.__getattribute__(data, '_local__key')
print current_thread().__dict__.get(key)

如何去理解源碼


現(xiàn)在可以靜下心來讀讀這不到兩百行的代碼了,不過,等等,好像有許多奇怪的內(nèi)容(黑魔法):

這些是什么?如果你不知道,沒關(guān)系坞琴,千萬不要被這些紙老虎嚇到,我們有豐富的文檔逗抑,查文檔就對了(這里不建議直接去網(wǎng)上搜相關(guān)關(guān)鍵字剧辐,最好是先讀文檔,讀完了有疑問再去搜)邮府。

python 黑魔法


下面是我對上面提到的內(nèi)容的一點總結(jié)忍啤,如果覺得讀的明白,那么可以繼續(xù)往下分析源碼了仙辟。如果還有不理解的同波,再讀幾遍文檔(或者我錯了参萄,歡迎指出來)。

  • 簡單來說煎饼,python 中創(chuàng)建一個新式類的實例時,首先會調(diào)用__new__(cls[, ...])創(chuàng)建實例筒溃,如果它成功返回cls類型的對象马篮,然后才會調(diào)用__init__來對對象進行初始化。
  • 新式類中我們可以用__slots__指定該類可以擁有的屬性名稱怜奖,這樣每個對象就不會再創(chuàng)建__dict__浑测,從而節(jié)省對象占用的空間。特別需要注意的是歪玲,基類的__slots__并不會屏蔽派生類中dict的創(chuàng)建迁央。
  • 可以通過重載__setattr____delattr____getattribute__這些方法滥崩,來控制自定義類的屬性訪問(x.name)岖圈,它們分別對應(yīng)屬性的賦值,刪除钙皮,讀取蜂科。
  • 鎖是操作系統(tǒng)中為了保證操作原子性而引入的概念,python 中 RLock是一種可重入鎖(reentrant lock短条,也可以叫作遞歸鎖)导匣,Rlock.acquire()可以不被阻塞地多次進入同一個線程。
  • __dict__用來保存對象的(可寫)屬性茸时,可以是一個字典贡定,或者其他映射對象。

源碼剖析


對這些相關(guān)的知識有了大概的了解后可都,再讀源碼就親切了很多厕氨。為了徹底理解,我們首先回想下平時是如何使用local對象的汹粤,然后分析源碼在背后的調(diào)用流程命斧。這里從定義一個最簡單的thread-local對象開始,也就是說當(dāng)我們寫下下面這句時嘱兼,發(fā)生了什么国葬?

data = local()

上面這句會調(diào)用 _localbase.__new__ 來為data對象設(shè)置一些屬性(還不知道有些屬性是做什么的,不要怕芹壕,后面遇見再說)汇四,然后將data的屬性字典(__dict__)作為當(dāng)前線程的一個屬性值(這個屬性的 key 是根據(jù) id(data) 生成的身份識別碼)。

這里很值得玩味:在創(chuàng)建ThreadLocal對象時踢涌,同時在線程(也是一個對象通孽,沒錯萬物皆對象)的屬性字典__dict__里面保存了ThreadLocal對象的屬性字典。還記得文章開始的圖片嗎睁壁,紅色虛線就表示這個操作背苦。

接著我們考慮在線程 Thread-1 中對ThreadLocal變量進行一些常用的操作互捌,比如下面的一個操作序列:

data.name = "Thread 1(main)" # 調(diào)用 __setattr__
print data.name     # 調(diào)用 __getattribute__
del data.name       # 調(diào)用 __delattr__
print data.__dict__
# Thread 1(main)
# {}

那么背后又是如何操作的呢?上面的操作包括了給屬性賦值行剂,讀屬性值秕噪,刪除屬性。這里我們以__getattribute__的實現(xiàn)為例(讀取值)進行分析厚宰,屬性的__setattr____delattr__和前者差不多腌巾,區(qū)別在于禁止了對__dict__屬性的更改以及刪除操作。

def __getattribute__(self, name):
    lock = object.__getattribute__(self, '_local__lock')
    lock.acquire()
    try:
        _patch(self)
        return object.__getattribute__(self, name)
    finally:
        lock.release()

函數(shù)中首先獲得了ThreadLocal變量的_local__lock屬性值(知道這個變量從哪里來的嗎铲觉,回顧下_localbase吧)澈蝙,然后用它來保證 _patch(self) 操作的原子性,還用 try-finally 保證即使拋出了異常也會釋放鎖資源撵幽,避免了線程意外情況下永久持有鎖而導(dǎo)致死鎖〉朴現(xiàn)在問題是_patch究竟做了什么?答案還是在源碼中:

def _patch(self):
    key = object.__getattribute__(self, '_local__key')  # ThreadLocal變量 的標(biāo)識符
    d = current_thread().__dict__.get(key)  # ThreadLocal變量在該線程下的數(shù)據(jù)
    if d is None:
        d = {}
        current_thread().__dict__[key] = d
        object.__setattr__(self, '__dict__', d)

        # we have a new instance dict, so call out __init__ if we have one
        cls = type(self)
        if cls.__init__ is not object.__init__:
            args, kw = object.__getattribute__(self, '_local__args')
            cls.__init__(self, *args, **kw)
    else:
        object.__setattr__(self, '__dict__', d)

_patch做的正是整個ThreadLocal實現(xiàn)中最核心的部分并齐,從當(dāng)前正在執(zhí)行的線程對象那里拿到該線程的私有數(shù)據(jù)漏麦,然后將其交給ThreadLocal變量客税,就是本文開始圖片中的虛線2况褪。這里需要補充說明以下幾點:

  • 這里說的線程的私有數(shù)據(jù),其實就是指通過x.name可以拿到的數(shù)據(jù)(其中 x 為ThreadLocal變量)
  • 主線程中在創(chuàng)建ThreadLocal對象后更耻,就有了對應(yīng)的數(shù)據(jù)(還記得紅色虛線的意義嗎测垛?)
  • 對于那些第一次訪問ThreadLocal變量的線程來說,需要創(chuàng)建一個空的字典來保存私有數(shù)據(jù)秧均,然后還要調(diào)用該變量的初始化函數(shù)食侮。
  • 還記得_localbase基類里new函數(shù)設(shè)置的屬性 _local__args 嗎?在這里被用來進行初始化目胡。

到此锯七,整個源碼核心部分已經(jīng)理解的差不多了,只剩下local.__del__用來執(zhí)行清除工作誉己。因為每次創(chuàng)建一個ThreadLocal 變量眉尸,都會在進程對象的__dict__中添加相應(yīng)的數(shù)據(jù),當(dāng)該變量被回收時巨双,我們需要在相應(yīng)的線程中刪除保存的對應(yīng)數(shù)據(jù)噪猾。

從源碼中學(xué)到了什么?


經(jīng)過一番努力筑累,終于揭開了 ThreadLocal 的神秘面紗袱蜡,整個過程可以說是收獲頗豐,下面一一說來慢宗。

不得不承認坪蚁,計算機基礎(chǔ)知識很重要奔穿。你得知道進程、線程是什么迅细,CPU 的工作機制巫橄,什么是操作的原子性,鎖是什么茵典,為什么鎖使用不當(dāng)會導(dǎo)致死鎖等等湘换。

其次就是語言層面的知識也必不可少,就ThreadLocal的實現(xiàn)來說统阿,如果對__new__彩倚,__slots__等不了解,根本不知道如何去做扶平。所以帆离,學(xué)語言還是要有深度,不然下面的代碼都看不懂:

class dict_test:
    pass

d = dict_test()
print d.__dict__
d.__dict__ = {'name': 'Jack', 'value': 12}
print d.name

還有就是高質(zhì)量的功能實現(xiàn)需要考慮各方各面的因素结澄,以ThreadLocal 為例哥谷,在基類_localbase中用 __slots__ 節(jié)省空間,用try_finally保證異常環(huán)境也能正常釋放鎖麻献,最后還用__del__來及時的清除無效的信息们妥。

最后不得不說,好的文檔和注釋簡直就是畫龍點睛勉吻,不過寫文檔和注釋是門技術(shù)活监婶,絕對需要不斷學(xué)習(xí)的。


PyChina將聯(lián)合JetBrain(出品PyCharm的公司)一起在北京舉辦一次Python沙龍活動齿桃。

時間:11月26日晚上19:00-21:00

地點:科技寺北新橋 北京市東城區(qū)東四北大街107號科林大廈B座107室(近北新橋地鐵站)

歡迎大家報名參加本次活動惑惶,特別需要志愿者來幫忙組織本次活動。

詳情請點擊此處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末短纵,一起剝皮案震驚了整個濱河市带污,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌香到,老刑警劉巖鱼冀,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異养渴,居然都是意外死亡雷绢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門理卑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翘紊,“玉大人,你說我怎么就攤上這事藐唠》保” “怎么了鹉究?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長踪宠。 經(jīng)常有香客問我自赔,道長,這世上最難降的妖魔是什么柳琢? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任绍妨,我火速辦了婚禮,結(jié)果婚禮上柬脸,老公的妹妹穿的比我還像新娘他去。我一直安慰自己,他們只是感情好倒堕,可當(dāng)我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布灾测。 她就那樣靜靜地躺著,像睡著了一般垦巴。 火紅的嫁衣襯著肌膚如雪媳搪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天骤宣,我揣著相機與錄音秦爆,去河邊找鬼。 笑死涯雅,一個胖子當(dāng)著我的面吹牛鲜结,可吹牛的內(nèi)容都是我干的展运。 我是一名探鬼主播活逆,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拗胜!你這毒婦竟也來了蔗候?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤埂软,失蹤者是張志新(化名)和其女友劉穎锈遥,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體勘畔,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡所灸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了炫七。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爬立。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖万哪,靈堂內(nèi)的尸體忽然破棺而出侠驯,到底是詐尸還是另有隱情抡秆,我是刑警寧澤,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布吟策,位于F島的核電站儒士,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏檩坚。R本人自食惡果不足惜着撩,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望匾委。 院中可真熱鬧睹酌,春花似錦、人聲如沸剩檀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沪猴。三九已至辐啄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間运嗜,已是汗流浹背壶辜。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留担租,地道東北人砸民。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像奋救,于是被迫代替她去往敵國和親岭参。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內(nèi)容