作者: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室(近北新橋地鐵站)
歡迎大家報名參加本次活動惑惶,特別需要志愿者來幫忙組織本次活動。
詳情請點擊此處