在 深刻理解Python中的ThreadLocal變量(上) 中咱們看到 ThreadLocal 的引入,使得能夠很方便地在多線程環境中使用局部變量。如此美妙的功能究竟是怎樣實現的?若是你對它的實現原理沒有好奇心或一探究竟的衝動,那麼接下來的內容估計會讓你後悔本身的淺嘗輒止了。html
簡單來講,Python 中 ThreadLocal 就是經過下圖中的方法,將全局變量假裝成線程局部變量,相信讀完本篇文章你會理解圖中內容的。(對這張圖不眼熟的話,能夠回顧下上篇))。python
好了,終於要來分析 ThreadLocal 是如何實現的啦,不過,等等,怎麼找到它的源碼呢?上一篇中咱們只是用過它(from threading import local
),從這裏只能看出它是在 threading 模塊實現的,那麼如何找到 threading 模塊的源碼呢。c++
若是你在使用 PyCharm,恭喜你,你能夠用 View source
(OS X 快捷鍵是 ⌘↓)找到 local 定義的地方。如今許多 IDE 都有這個功能,能夠查看 IDE 的幫助來找到該功能。接着咱們就會發現 local 是這樣子的(這裏以 python 2.7 爲例):git
# 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
嗯,自帶解釋,很是好。咱們要作的是繼續往下深挖具體實現,用一樣的方法(⌘↓)找 _local 的實現,好像不太妙,沒有找到純 python 實現:github
class _local(object): """ Thread-local data """ def __delattr__(self, name): # real signature unknown; restored from __doc__ """ x.__delattr__('name') <==> del x.name """ pass ...
不要緊,繼續來看下_threading_local吧,這下子終於找到了local的純 python 實現。開始就是很長的一段註釋文檔,告訴咱們這個模塊是什麼,如何用。這個文檔的質量很是高,值得咱們去學習。因此,再次後悔本身的淺嘗輒止了吧,差點錯過了這麼優秀的文檔範文!多線程
在具體動手分析這個模塊以前,咱們先把它拷出來放在一個單獨的文件 thread_local.py
中,這樣能夠方便咱們隨意肢解它(好比在適當的地方加上log),並用修改後的實現驗證咱們的一些想法。此外,若是你真的理解了_threading_local.py最開始的一段,你就會發現這樣作是多麼的有必要。由於python的threading.local不必定是用的_threading_local(還記得class _local(object) 嗎?)。函數
因此若是你用 threading.local 來驗證本身對_threading_local.py的理解,你極可能會一頭霧水的。不幸的是,我開始就這樣乾的,因此被下面的代碼坑了很久:post
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'
固然,你可能不理解這裏是什麼意思,不要緊,我只是想強調在 threading.local 沒有用到_threading_local.py,你必需要建立一個模塊(我將它命名爲 thread_local.py)來保存_threading_local裏面的內容,而後像下面這樣驗證本身的想法:學習
from threading import current_thread from thread_local import local data = local() key = object.__getattribute__(data, '_local__key') print current_thread().__dict__.get(key)
如今能夠靜下心來讀讀這不到兩百行的代碼了,不過,等等,好像有許多奇怪的內容(黑魔法):ui
這些是什麼?若是你不知道,不要緊,千萬不要被這些紙老虎嚇到,咱們有豐富的文檔,查文檔就對了(這裏不建議直接去網上搜相關關鍵字,最好是先讀文檔,讀完了有疑問再去搜)。
下面是我對上面提到的內容的一點總結,若是以爲讀的明白,那麼能夠繼續往下分析源碼了。若是還有不理解的,再讀幾遍文檔(或者我錯了,歡迎指出來)。
簡單來講,python 中建立一個新式類的實例時,首先會調用__new__(cls[, ...])
建立實例,若是它成功返回cls類型的對象,而後纔會調用__init__來對對象進行初始化。
新式類中咱們能夠用__slots__指定該類能夠擁有的屬性名稱,這樣每一個對象就不會再建立__dict__,從而節省對象佔用的空間。特別須要注意的是,基類的__slots__並不會屏蔽派生類中__dict__的建立。
能夠經過重載__setattr__,__delattr__和__getattribute__
這些方法,來控制自定義類的屬性訪問(x.name),它們分別對應屬性的賦值,刪除,讀取。
鎖是操做系統中爲了保證操做原子性而引入的概念,python 中 RLock是一種可重入鎖(reentrant lock,也能夠叫做遞歸鎖),Rlock.acquire()能夠不被阻塞地屢次進入同一個線程。
__dict__
用來保存對象的(可寫)屬性,能夠是一個字典,或者其餘映射對象。
對這些相關的知識有了大概的瞭解後,再讀源碼就親切了不少。爲了完全理解,咱們首先回想下平時是如何使用local對象的,而後分析源碼在背後的調用流程。這裏從定義一個最簡單的thread-local對象開始,也就是說當咱們寫下下面這句時,發生了什麼?
data = local()
上面這句會調用 _localbase.__new__
來爲data對象設置一些屬性(還不知道有些屬性是作什麼的,不要怕,後面碰見再說),而後將data的屬性字典(__dict__
)做爲當前線程的一個屬性值(這個屬性的 key 是根據 id(data) 生成的身份識別碼)。
這裏很值得玩味:在建立ThreadLocal對象時,同時在線程(也是一個對象,沒錯萬物皆對象)的屬性字典__dict__
裏面保存了ThreadLocal對象的屬性字典。還記得文章開始的圖片嗎,紅色虛線就表示這個操做。
接着咱們考慮在線程 Thread-1 中對ThreadLocal變量進行一些經常使用的操做,好比下面的一個操做序列:
data.name = "Thread 1(main)" # 調用 __setattr__ print data.name # 調用 __getattribute__ del data.name # 調用 __delattr__ print data.__dict__ # Thread 1(main) # {}
那麼背後又是如何操做的呢?上面的操做包括了給屬性賦值,讀屬性值,刪除屬性。這裏咱們以__getattribute__的實現爲例(讀取值)進行分析,屬性的__setattr__和__delattr__和前者差很少,區別在於禁止了對__dict__屬性的更改以及刪除操做。
def __getattribute__(self, name): lock = object.__getattribute__(self, '_local__lock') lock.acquire() try: _patch(self) return object.__getattribute__(self, name) finally: lock.release()
函數中首先得到了ThreadLocal變量的_local__lock
屬性值(知道這個變量從哪裏來的嗎,回顧下_localbase吧),而後用它來保證 _patch(self)
操做的原子性,還用 try-finally 保證即便拋出了異常也會釋放鎖資源,避免了線程意外狀況下永久持有鎖而致使死鎖。如今問題是_patch究竟作了什麼?答案仍是在源碼中:
def _patch(self): key = object.__getattribute__(self, '_local__key') # ThreadLocal變量 的標識符 d = current_thread().__dict__.get(key) # ThreadLocal變量在該線程下的數據 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實現中最核心的部分,從當前正在執行的線程對象那裏拿到該線程的私有數據,而後將其交給ThreadLocal變量,就是本文開始圖片中的虛線2。這裏須要補充說明如下幾點:
這裏說的線程的私有數據,其實就是指經過x.name能夠拿到的數據(其中 x 爲ThreadLocal變量)
主線程中在建立ThreadLocal對象後,就有了對應的數據(還記得紅色虛線的意義嗎?)
對於那些第一次訪問ThreadLocal變量的線程來講,須要建立一個空的字典來保存私有數據,而後還要調用該變量的初始化函數。
還記得_localbase基類裏__new__函數設置的屬性 _local__args 嗎?在這裏被用來進行初始化。
到此,整個源碼核心部分已經理解的差很少了,只剩下local.__del__
用來執行清除工做。由於每次建立一個ThreadLocal 變量,都會在進程對象的__dict__中添加相應的數據,當該變量被回收時,咱們須要在相應的線程中刪除保存的對應數據。
通過一番努力,終於揭開了 ThreadLocal 的神祕面紗,整個過程能夠說是收穫頗豐,下面一一說來。
不得不認可,計算機基礎知識很重要。你得知道進程、線程是什麼,CPU 的工做機制,什麼是操做的原子性,鎖是什麼,爲何鎖使用不當會致使死鎖等等。
其次就是語言層面的知識也必不可少,就ThreadLocal的實現來講,若是對__new__,__slots__等不瞭解,根本不知道如何去作。因此,學語言仍是要有深度,否則下面的代碼都看不懂:
class dict_test: pass d = dict_test() print d.__dict__ d.__dict__ = {'name': 'Jack', 'value': 12} print d.name
還有就是高質量的功能實現須要考慮各方各面的因素,以ThreadLocal 爲例,在基類_localbase中用__slots__節省空間,用try_finally保證異常環境也能正常釋放鎖,最後還用__del__來及時的清除無效的信息。
最後不得不說,好的文檔和註釋簡直就是畫龍點睛,不過寫文檔和註釋是門技術活,絕對須要不斷學習的。
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 全面分析
我是如何閱讀開源項目的源代碼的
高效閱讀源代碼指南
如何閱讀程序源代碼?
如何看懂源代碼--(分析源代碼方法)
本文由selfboot 發表於我的博客,採用署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議。
非商業轉載請註明做者及出處。商業轉載請聯繫做者本人
本文標題爲:深刻理解Python中的ThreadLocal變量(中)
本文連接爲:http://selfboot.cn/2016/08/26...