摘要:在深入理解中的變量上中我們看到的引入,使得可以很方便地在多線程環(huán)境中使用局部變量。特別需要注意的是,基類的并不會(huì)屏蔽派生類中的創(chuàng)建。到此,整個(gè)源碼核心部分已經(jīng)理解的差不多了,只剩下用來(lái)執(zhí)行清除工作。
在 深入理解Python中的ThreadLocal變量(上) 中我們看到 ThreadLocal 的引入,使得可以很方便地在多線程環(huán)境中使用局部變量。如此美妙的功能到底是怎樣實(shí)現(xiàn)的?如果你對(duì)它的實(shí)現(xiàn)原理沒(méi)有好奇心或一探究竟的沖動(dòng),那么接下來(lái)的內(nèi)容估計(jì)會(huì)讓你后悔自己的淺嘗輒止了。
簡(jiǎn)單來(lái)說(shuō),Python 中 ThreadLocal 就是通過(guò)下圖中的方法,將全局變量偽裝成線程局部變量,相信讀完本篇文章你會(huì)理解圖中內(nèi)容的。(對(duì)這張圖不眼熟的話,可以回顧下上篇))。
在哪里找到源碼?好了,終于要來(lái)分析 ThreadLocal 是如何實(shí)現(xiàn)的啦,不過(guò),等等,怎么找到它的源碼呢?上一篇中我們只是用過(guò)它(from threading import local),從這里只能看出它是在 threading 模塊實(shí)現(xiàn)的,那么如何找到 threading 模塊的源碼呢。
如果你在使用 PyCharm,恭喜你,你可以用 View source(OS X 快捷鍵是 ?↓)找到 local 定義的地方?,F(xiàn)在許多 IDE 都有這個(gè)功能,可以查看 IDE 的幫助來(lái)找到該功能。接著我們就會(huì)發(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ù)往下深挖具體實(shí)現(xiàn),用同樣的方法(?↓)找 _local 的實(shí)現(xiàn),好像不太妙,沒(méi)有找到純 python 實(shí)現(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 ...
沒(méi)關(guān)系,繼續(xù)來(lái)看下_threading_local吧,這下子終于找到了local的純 python 實(shí)現(xiàn)。開(kāi)始就是很長(zhǎng)的一段注釋文檔,告訴我們這個(gè)模塊是什么,如何用。這個(gè)文檔的質(zhì)量非常高,值得我們?nèi)W(xué)習(xí)。所以,再次后悔自己的淺嘗輒止了吧,差點(diǎn)錯(cuò)過(guò)了這么優(yōu)秀的文檔范文!
將源碼私有化在具體動(dòng)手分析這個(gè)模塊之前,我們先把它拷出來(lái)放在一個(gè)多帶帶的文件 thread_local.py 中,這樣可以方便我們隨意肢解它(比如在適當(dāng)?shù)牡胤郊由蟣og),并用修改后的實(shí)現(xiàn)驗(yàn)證我們的一些想法。此外,如果你真的理解了_threading_local.py最開(kāi)始的一段,你就會(huì)發(fā)現(xiàn)這樣做是多么的有必要。因?yàn)閜ython的threading.local不一定是用的_threading_local(還記得class _local(object) 嗎?)。
所以如果你用 threading.local 來(lái)驗(yàn)證自己對(duì)_threading_local.py的理解,你很可能會(huì)一頭霧水的。不幸的是,我開(kāi)始就這樣干的,所以被下面的代碼坑了好久:
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)然,你可能不理解這里是什么意思,沒(méi)關(guān)系,我只是想強(qiáng)調(diào)在 threading.local 沒(méi)有用到_threading_local.py,你必須要?jiǎng)?chuàng)建一個(gè)模塊(我將它命名為 thread_local.py)來(lái)保存_threading_local里面的內(nèi)容,然后像下面這樣驗(yàn)證自己的想法:
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)在可以靜下心來(lái)讀讀這不到兩百行的代碼了,不過(guò),等等,好像有許多奇怪的內(nèi)容(黑魔法):
__slots__
__new__
__getattribute__/__setattr__/__delattr__
Rlock
這些是什么?如果你不知道,沒(méi)關(guān)系,千萬(wàn)不要被這些紙老虎嚇到,我們有豐富的文檔,查文檔就對(duì)了(這里不建議直接去網(wǎng)上搜相關(guān)關(guān)鍵字,最好是先讀文檔,讀完了有疑問(wèn)再去搜)。
python 黑魔法下面是我對(duì)上面提到的內(nèi)容的一點(diǎn)總結(jié),如果覺(jué)得讀的明白,那么可以繼續(xù)往下分析源碼了。如果還有不理解的,再讀幾遍文檔(或者我錯(cuò)了,歡迎指出來(lái))。
簡(jiǎn)單來(lái)說(shuō),python 中創(chuàng)建一個(gè)新式類的實(shí)例時(shí),首先會(huì)調(diào)用__new__(cls[, ...])創(chuàng)建實(shí)例,如果它成功返回cls類型的對(duì)象,然后才會(huì)調(diào)用__init__來(lái)對(duì)對(duì)象進(jìn)行初始化。
新式類中我們可以用__slots__指定該類可以擁有的屬性名稱,這樣每個(gè)對(duì)象就不會(huì)再創(chuàng)建__dict__,從而節(jié)省對(duì)象占用的空間。特別需要注意的是,基類的__slots__并不會(huì)屏蔽派生類中__dict__的創(chuàng)建。
可以通過(guò)重載__setattr__,__delattr__和__getattribute__這些方法,來(lái)控制自定義類的屬性訪問(wèn)(x.name),它們分別對(duì)應(yīng)屬性的賦值,刪除,讀取。
鎖是操作系統(tǒng)中為了保證操作原子性而引入的概念,python 中 RLock是一種可重入鎖(reentrant lock,也可以叫作遞歸鎖),Rlock.acquire()可以不被阻塞地多次進(jìn)入同一個(gè)線程。
__dict__用來(lái)保存對(duì)象的(可寫(xiě))屬性,可以是一個(gè)字典,或者其他映射對(duì)象。
源碼剖析對(duì)這些相關(guān)的知識(shí)有了大概的了解后,再讀源碼就親切了很多。為了徹底理解,我們首先回想下平時(shí)是如何使用local對(duì)象的,然后分析源碼在背后的調(diào)用流程。這里從定義一個(gè)最簡(jiǎn)單的thread-local對(duì)象開(kāi)始,也就是說(shuō)當(dāng)我們寫(xiě)下下面這句時(shí),發(fā)生了什么?
data = local()
上面這句會(huì)調(diào)用 _localbase.__new__ 來(lái)為data對(duì)象設(shè)置一些屬性(還不知道有些屬性是做什么的,不要怕,后面遇見(jiàn)再說(shuō)),然后將data的屬性字典(__dict__)作為當(dāng)前線程的一個(gè)屬性值(這個(gè)屬性的 key 是根據(jù) id(data) 生成的身份識(shí)別碼)。
這里很值得玩味:在創(chuàng)建ThreadLocal對(duì)象時(shí),同時(shí)在線程(也是一個(gè)對(duì)象,沒(méi)錯(cuò)萬(wàn)物皆對(duì)象)的屬性字典__dict__里面保存了ThreadLocal對(duì)象的屬性字典。還記得文章開(kāi)始的圖片嗎,紅色虛線就表示這個(gè)操作。
接著我們考慮在線程 Thread-1 中對(duì)ThreadLocal變量進(jìn)行一些常用的操作,比如下面的一個(gè)操作序列:
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__的實(shí)現(xiàn)為例(讀取值)進(jìn)行分析,屬性的__setattr__和__delattr__和前者差不多,區(qū)別在于禁止了對(duì)__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屬性值(知道這個(gè)變量從哪里來(lái)的嗎,回顧下_localbase吧),然后用它來(lái)保證 _patch(self) 操作的原子性,還用 try-finally 保證即使拋出了異常也會(huì)釋放鎖資源,避免了線程意外情況下永久持有鎖而導(dǎo)致死鎖。現(xiàn)在問(wèn)題是_patch究竟做了什么?答案還是在源碼中:
def _patch(self): key = object.__getattribute__(self, "_local__key") # ThreadLocal變量 的標(biāo)識(shí)符 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做的正是整個(gè)ThreadLocal實(shí)現(xiàn)中最核心的部分,從當(dāng)前正在執(zhí)行的線程對(duì)象那里拿到該線程的私有數(shù)據(jù),然后將其交給ThreadLocal變量,就是本文開(kāi)始圖片中的虛線2。這里需要補(bǔ)充說(shuō)明以下幾點(diǎn):
這里說(shuō)的線程的私有數(shù)據(jù),其實(shí)就是指通過(guò)x.name可以拿到的數(shù)據(jù)(其中 x 為ThreadLocal變量)
主線程中在創(chuàng)建ThreadLocal對(duì)象后,就有了對(duì)應(yīng)的數(shù)據(jù)(還記得紅色虛線的意義嗎?)
對(duì)于那些第一次訪問(wèn)ThreadLocal變量的線程來(lái)說(shuō),需要?jiǎng)?chuàng)建一個(gè)空的字典來(lái)保存私有數(shù)據(jù),然后還要調(diào)用該變量的初始化函數(shù)。
還記得_localbase基類里__new__函數(shù)設(shè)置的屬性 _local__args 嗎?在這里被用來(lái)進(jìn)行初始化。
到此,整個(gè)源碼核心部分已經(jīng)理解的差不多了,只剩下local.__del__用來(lái)執(zhí)行清除工作。因?yàn)槊看蝿?chuàng)建一個(gè)ThreadLocal 變量,都會(huì)在進(jìn)程對(duì)象的__dict__中添加相應(yīng)的數(shù)據(jù),當(dāng)該變量被回收時(shí),我們需要在相應(yīng)的線程中刪除保存的對(duì)應(yīng)數(shù)據(jù)。
從源碼中學(xué)到了什么?經(jīng)過(guò)一番努力,終于揭開(kāi)了 ThreadLocal 的神秘面紗,整個(gè)過(guò)程可以說(shuō)是收獲頗豐,下面一一說(shuō)來(lái)。
不得不承認(rèn),計(jì)算機(jī)基礎(chǔ)知識(shí)很重要。你得知道進(jìn)程、線程是什么,CPU 的工作機(jī)制,什么是操作的原子性,鎖是什么,為什么鎖使用不當(dāng)會(huì)導(dǎo)致死鎖等等。
其次就是語(yǔ)言層面的知識(shí)也必不可少,就ThreadLocal的實(shí)現(xiàn)來(lái)說(shuō),如果對(duì)__new__,__slots__等不了解,根本不知道如何去做。所以,學(xué)語(yǔ)言還是要有深度,不然下面的代碼都看不懂:
class dict_test: pass d = dict_test() print d.__dict__ d.__dict__ = {"name": "Jack", "value": 12} print d.name
還有就是高質(zhì)量的功能實(shí)現(xiàn)需要考慮各方各面的因素,以ThreadLocal 為例,在基類_localbase中用__slots__節(jié)省空間,用try_finally保證異常環(huán)境也能正常釋放鎖,最后還用__del__來(lái)及時(shí)的清除無(wú)效的信息。
最后不得不說(shuō),好的文檔和注釋簡(jiǎn)直就是畫(huà)龍點(diǎn)睛,不過(guò)寫(xiě)文檔和注釋是門技術(shù)活,絕對(duì)需要不斷學(xué)習(xí)的。
更多閱讀Python"s use of __new__ and __init__?
Understanding __new__ and __init__
Usage of __slots__?
weakref – Garbage-collectable references to objects
How do I find the source code of a function in Python?
How do I find the location of Python module sources?
Is self.__dict__.update(**kwargs) good or poor style?
Doc: weakref — Weak references
python class 全面分析
我是如何閱讀開(kāi)源項(xiàng)目的源代碼的
高效閱讀源代碼指南
如何閱讀程序源代碼?
如何看懂源代碼--(分析源代碼方法)
本文由selfboot 發(fā)表于個(gè)人博客,采用署名-非商業(yè)性使用-相同方式共享 3.0 中國(guó)大陸許可協(xié)議。
非商業(yè)轉(zhuǎn)載請(qǐng)注明作者及出處。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者本人
本文標(biāo)題為:深入理解Python中的ThreadLocal變量(中)
本文鏈接為:http://selfboot.cn/2016/08/26...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/38122.html
摘要:我們知道多線程環(huán)境下,每一個(gè)線程均可以使用所屬進(jìn)程的全局變量。在線程中使用局部變量則不存在這個(gè)問(wèn)題,因?yàn)槊總€(gè)線程的局部變量不能被其他線程訪問(wèn)。 我們知道多線程環(huán)境下,每一個(gè)線程均可以使用所屬進(jìn)程的全局變量。如果一個(gè)線程對(duì)全局變量進(jìn)行了修改,將會(huì)影響到其他所有的線程。為了避免多個(gè)線程同時(shí)對(duì)變量進(jìn)行修改,引入了線程同步機(jī)制,通過(guò)互斥鎖,條件變量或者讀寫(xiě)鎖來(lái)控制對(duì)全局變量的訪問(wèn)。 只用全局變...
摘要:具體怎么實(shí)現(xiàn)的呢,思想其實(shí)特別簡(jiǎn)單,我們?cè)谏钊肜斫庵械淖兞可弦晃牡淖詈笥刑崞疬^(guò),就是創(chuàng)建一個(gè)全局字典,然后將線程或者協(xié)程標(biāo)識(shí)符作為,相應(yīng)線程或協(xié)程的局部數(shù)據(jù)作為。 在上篇我們看到了 ThreadLocal 變量的簡(jiǎn)單使用,中篇對(duì)python中 ThreadLocal 的實(shí)現(xiàn)進(jìn)行了分析,但故事還沒(méi)有結(jié)束。本篇我們一起來(lái)看下Werkzeug中ThreadLocal的設(shè)計(jì)。 Werkzeug...
摘要:中的詳解必修個(gè)多線程問(wèn)題總結(jié)個(gè)多線程問(wèn)題總結(jié)有哪些源代碼看了后讓你收獲很多,代碼思維和能力有較大的提升有哪些源代碼看了后讓你收獲很多,代碼思維和能力有較大的提升開(kāi)源的運(yùn)行原理從虛擬機(jī)工作流程看運(yùn)行原理。 自己實(shí)現(xiàn)集合框架 (三): 單鏈表的實(shí)現(xiàn) 自己實(shí)現(xiàn)集合框架 (三): 單鏈表的實(shí)現(xiàn) 基于 POI 封裝 ExcelUtil 精簡(jiǎn)的 Excel 導(dǎo)入導(dǎo)出 由于 poi 本身只是針對(duì)于 ...
摘要:方法,刪除當(dāng)前線程綁定的這個(gè)副本數(shù)字,這個(gè)值是的值,普通的是使用鏈表來(lái)處理沖突的,但是是使用線性探測(cè)法來(lái)處理沖突的,就是每次增加的步長(zhǎng),根據(jù)參考資料所說(shuō),選擇這個(gè)數(shù)字是為了讓沖突概率最小。 showImg(https://segmentfault.com/img/remote/1460000019828633); 老套路,先列舉下關(guān)于ThreadLocal常見(jiàn)的疑問(wèn),希望可以通過(guò)這篇學(xué)...
閱讀 1322·2023-04-25 17:05
閱讀 3079·2021-11-19 09:40
閱讀 3833·2021-11-18 10:02
閱讀 1820·2021-09-23 11:45
閱讀 3093·2021-08-20 09:36
閱讀 2846·2021-08-13 15:07
閱讀 1203·2019-08-30 15:55
閱讀 2534·2019-08-30 14:11