引言:
從本節開始的連續幾節我們都會圍繞著Python併發進行學習,
本節學習的是 threading
這個執行緒相關模組,附上官方文件:
docs.python.org/3/library/t…
跟官方文件走最穩健,網上的文章都是某一時期的產物,IT更新
換代那麼快,過了一段時間可能就改得面目全非了,然後你看了
小豬現在的文章然後寫程式碼,這不行那不行就開始噴起我來了,我表示

另外,在查閱相關資料的時候發現很多文章還是用的 thread模組
,
在高版本中已經使用threading來替代thread了!!!如果你在
Python 2.x版本想使用threading的話,可以使用dummy_threading
話不多說開始本節內容~
1.threaing模組提供的可直接呼叫函式
- active_count():獲取當前活躍(alive)執行緒的個數;
- current_thread():獲取當前的執行緒物件;
- get_ident():返回當前執行緒的索引,一個非零的整數;(3.3新增)
- enumerate():獲取當前所有活躍執行緒的列表;
- main_thread():返回主執行緒物件,(3.4新增);
- settrace(func):設定一個回撥函式,在run()執行之前被呼叫;
- setprofile(func):設定一個回撥函式,在run()執行完畢之後呼叫;
- stack_size():返回建立新執行緒時使用的執行緒堆疊大小;
- threading.TIMEOUT_MAX:堵塞執行緒時間最大值,超過這個值會棧溢位!
2.執行緒區域性變數(Thread-Local Data)
先說個知識點:
在一個程式內所有的執行緒共享程式的全域性變數,執行緒間共享資料很方便 但是每個執行緒都可以隨意修改全域性變數,可能會引起執行緒安全問題, 這個時候,可以對全域性變數進行加鎖來解決。對於執行緒私有資料可以 通過使用區域性變數,只有執行緒自身可以訪問,其他執行緒無法訪問, 除此之外,Python還給我們提供了ThreadLocal變數,本身是一個全域性 變數,但是執行緒們卻可以使用它來儲存私有資料。
用法也很簡單,定義一個全域性變數:data = thread.local(),然後就可以 往裡面存資料啦,比如data.num = xxx,寫個簡單例子來驗證下: 注:如果data沒有設定對應的屬性,直接取會報AttributeError異常, 使用時可以捕獲這個異常,或者先呼叫**hasattr(物件,屬性)**判斷物件中 是否有該屬性!

輸出結果:

厲害了,不同執行緒訪問果然是返回的不同值,小豬這種求知慾 旺盛的人肯定是要扒一波看看是怎麼實現的啦,跟原始碼會比較 枯燥,先簡單說下實現套路:
threading.local()例項化一個全域性物件,這個全域性物件裡有 一個大字典,鍵值為兩個弱引用物件 {執行緒物件,字典物件}, 然後可以通過current_thread()獲得當前的執行緒物件,然後根據 這個物件可以拿到對應的字典物件,然後進行引數的讀或者寫。
是的大概套路就是這樣,接下來就是剖析原始碼環節了,挺枯燥的, 可以不看,看的話,相信你會收穫非常多,小豬昨天下午開始看 _threading_local.py這個模組的原始碼,僅僅246行,卻看到了晚上 十點才捨得回家,收益頗豐,Get了N多知識點,至少在那些什麼 Python教程裡沒看到過,每弄懂一個都會忍不出發出:

這樣的感嘆!快上老司機小豬的車吧,上車只需五個滑稽幣:

*3._threading_local原始碼解析
按住ctrl點local()方法,會進到threading.py模組,會定位到這一行:

_thread 模組上節也說了threading模組的基礎模組,應該儘量使用 threading 模組替代,而我們程式碼裡也沒匯入這個模組,所以會走 _threading_local ,點進去看下這個模組,246行程式碼,不多,嘿嘿, 點選PyCharm左側的Structure看看程式碼結構

關注點在**_localimpl和local**兩個類上,我們先把這個模組的原始碼 全選,然後新建一個Python檔案,把內容貼上到裡面,為什麼要 這樣做呢?
答:因為這樣方便我們進行程式碼執行跟蹤啊,Debug除錯 或打Log跟蹤方法執行順序,或者檢視某個時刻某些變數的值!
很多小夥伴可能只會print不會使用Debug除錯,這裡順道簡單 介紹下怎麼用,掌握這個對跟原始碼非常有用,務必掌握!!!
1.PyCharm除錯速成
點選左側邊欄可以下斷點,在除錯模式下執行的話,執行到 這一行的時候會暫時掛起,並啟用偵錯程式視窗:

點選頂部的小蟲子標記即可進入除錯模式:


MainThread這個表示當前斷點上的執行緒,下面是該執行緒的堆疊幀 右側Variables是變數除錯視窗,可以檢視此時的變數情況! 接著就來一一說下一些除錯技巧吧:
單步除錯,

當單步除錯執行到某一個函式,如果你不想直接執行完,切到下 一行而是想看進去這個函式的執行過程的話,可以點選

上面這一步,遇到官方類庫的函式也會進去,如果只想在碰到 自己定義函式才進去的話,可以點選

進入函式後確定沒什麼問題了,可以點選

如果想快速執行到下一個斷點的位置,可以點選

跨斷點除錯,點選左側欄的:

監視變數,有時右側Variables,顯示的變數有很多時,而你 想關注某一個變數而已,可以點選這個小眼鏡:

停止除錯,點選左側紅色按鈕即可跳過除錯,不是停止程式!:

斷點設定,點選左側:

好的,關於PyCharm除錯就先說這麼多,基本夠用了,
回到我們的原始碼,我們使用了threading.local()初始化了例項,
按照我們第一節學的類內容,類會走建構函式__init__()
對吧?
然而,在local類裡,並沒有發現這個函式,只有一個__new__(cls, *args, **kw)
,
這又是一個新的知識點了!
2.Python中的經典類和新式類
在Python 2.x中預設都是經典類,除非顯式繼承object才是新式類;
而在Python 3.x中預設都是新式類,不用顯式繼承object;
新式類相比經典類增加了很多內建屬性,比如**__class__
**
獲得自身型別(type),**__slots__
**內建屬性,還有這裡的
new()函式等。
3.__new__()
函式
在呼叫**init()方法前,new(cls, args, kw)可決定是否使用該 init()方法,可以呼叫其他類的構造方法或者直接返回別的物件 來作為本類的例項。cls表示需要例項化的類,該引數在例項化時由 Python直譯器自動提供。另外還要注意一點,new必須有返回值, 可以返回父類__new__()出來的例項 或 object的__new__()出來的例項 如果__new__()沒有成功返回cls型別的物件,是不會呼叫*init**() 來對物件進行初始化的!!!
臥槽,騷氣,程式碼裡也剛好這樣做了,返回的是一個**_localimpl()**物件:

直接例項化的**_localimpl(),然後設定了localargs**,locallock 以及呼叫了create_dict()方法。先定位到_localimpl類的localargs

又觸發新知識點:黑魔法__slots__
4.Python黑魔法__slots__
內建屬性
作用是阻止在例項化類時為例項分配dict,使用這個東西會帶來: 更快的屬性訪問速度 和 減少記憶體消耗。此話怎麼說?
預設情況下,Python的類例項都會有一個**dict來儲存例項的屬性, 注意:只儲存例項的變數,不會儲存類屬性!!! 可以呼叫內建屬性dict**進行訪問,比如下面的例子:

輸出結果:

看上去是挺靈活的,在程式裡可以隨意設定新屬性,只是每次 例項化類物件的時候,都需要去分配一個新的dict,如果是對於 擁有固定屬性的class來說,這就有點浪費記憶體了,特別是在需要 建立大量例項的時候,這個問題就尤為突出了。Python在新式類中給 我們提供了**slots屬性來幫助我們解決這個問題。 slots是一個元組,包括了當前能訪問到的屬性,定義後 slots中定義的變數變成了類的描述符**,相當於java裡的成員變數 宣告,不能再增加新的變數。還有一點要注意: 定義定義了__slots__後,就不再有__dict__!!!可以寫個例子驗證下:

輸出結果:

Python內建的dict(字典) 本質是一個雜湊表,通過空間換時間, 在例項化物件達到萬級,和**slots用元組**對比耗費的記憶體就不是 一點半點了!另外屬性訪問速度也是比dict快的,相關對比以及 更多內容可見:www.cnblogs.com/rainfd/p/sl… 和:Saving 9 GB of RAM with Python’s slots
瞭解完**slots後,我們回到我們的原始碼,回到_localimpl的init()**

設定了一個key,規則是:_threading_local._localimpl. 拼接上物件所在的記憶體地址 這裡的id()函式作用是獲得物件的記憶體地址。接著初始化了一個dicts,大詞典, 拿來存放鍵值對的:(弱引用的執行緒物件,該執行緒在_localimpl物件裡對應的資料字典) 就是每個執行緒物件,對應_localimp裡不同的字典物件,這些字典物件都放在 大字典裡。
接著回到local類的**new()** 函式,這裡是一個設定屬性的方法:

_local__impl屬性在上面通過**slots**定義了

簡單點理解就是為local設定了一個**_localimpl物件,後面 可以根據根據這個name = _local__impl拿到對應的_localimpl**物件!
而且這裡沒那麼簡單,local類裡對這個函式進行了重寫:

這裡前面判斷name是否為__dict__,猜測是許可權控制,不允許 外部通過**setattr或delattr**來操作字典,只允許通過 **_patch()**方法來修改操作字典!
接著繼續來跟下**_patch()**方法:

@contextmanager 又是什麼東西???

又是新的知識點~
5.@contextmanager
這就涉及到我們以前學習的with結構了,在爬蟲寫入檔案那裡用過, 不用自己寫finally,然後在裡面去close()檔案,以避免不必要的錯誤, 不知道你還記不記得,不記得的話回頭翻翻吧。
對於類似於檔案關閉這種不想遺忘的重要操作,我們可以自己封裝 一個with結構來進行處理,封裝也很簡單,再定義你那個類的時候 重寫**enter方法和exit**方法,比如檔案關閉那個可以自定義 成這樣的:

如果覺得上面這種實現起來比較麻煩的話,就可以用 @contextmanager啦,直接就一個方法,比定義類簡單多了~

知道@contextmanager之後,繼續來分析**_patch()方法,先根據 _local__impl這個值拿到了local裡的_localimpl物件,然後 呼叫impl的get_dict()**想獲得一個資料字典:

current_thread()獲得當前執行緒,然後獲得執行緒的記憶體地址,查詢dicts裡 此執行緒對應的字典,此時,如果dicts裡沒有這個執行緒對應的資料字典, 會引發KeyError異常,執行:

呼叫create_dict()方法建立字典:

建立空字典,設定key,獲得當前執行緒,獲得當前執行緒的記憶體地址; 就是做一些準備工作,接著看到定義了兩個方法,先跳過,往下看:

然後又是新的知識點:Python弱引用函式ref()!
6.Python弱引用函式ref()
ref()這個函式是weakref模組 提供的用於建立一個弱引用的函式, 引數異常是想建立弱引用的物件,當弱引用的物件被刪除後的回撥函式 為什麼要用弱引用?
Python和其他高階語言一樣,使用垃圾回收器來自動銷燬不再使用的物件, 每個物件都有一個引用計數,當這個計數為0時,Python才能夠安全地銷燬 這個物件,當物件只剩下弱引用時也會回收!
這裡的local_deleted()和thread_deleted() 這兩個回撥引數 就是在**_localimpl物件和執行緒物件**被回收時觸發:

localimpl物件被回收時把執行緒裡持有localimpl物件的弱引用刪除掉, 執行緒物件物件被回收時,彈出大字典中該執行緒對應的資料字典;
剩下的三句就是儲存_localimpl物件的弱引用到thread的**dict裡, localimpl物件新增鍵值對(執行緒弱引用,執行緒對應的資料字典)到 大字典中,然後返回執行緒對應的資料字典**。
又回到**_patch()方法,拿到引數,然後又呼叫init函式 然後呼叫了init函式,這裡不是很明白動機,猜測是如果 另外重寫了local的init**函式,可以呼叫一些其他的操作吧。

再接著又有一個知識點了,運算元據字典時的加鎖,正常來說 私用Lock或RLock,需要自己去呼叫acquire()和release(), 而使用with關鍵字,就無需你自己去操心了,原因是RLock 類裡重寫了**enter和exit**函式。

最後yield返回一個生成器物件。
到此,_threading_local模組的完整的原始碼實現套路就浮出水面了, 不錯,Get了很多新的姿勢,如果你還有些疑惑的話,可以自己Debug, 跟跟方法的呼叫順序,慢慢體會。
4.執行緒物件(threading.Thread)
使用threading.Thread
建立執行緒:
可以通過下面兩種方法建立新執行緒:
- 1.直接建立threading.Thread物件,並把呼叫物件作為引數傳入;
- 2.繼承threading.Thread類,**重寫run()**方法;
這裡寫程式碼測試個東西:到底使用多執行緒快還是單執行緒快~

兩次執行結果採集:

測試環境:Ubuntu 14.04 為了儘量公平,把單執行緒執行那個也另外放到 一個執行緒中,結果發現,多執行緒並沒有比單執行緒快,反而還慢了一些。 出現這個原因是以為Python中的:全域性直譯器鎖(GIL),上一節已經 介紹過了,這裡就不再複述了。
Thread類建構函式

引數依次是:
- group:執行緒組
- target:要執行的函式
- name:執行緒名字
- args/kwargs:要傳入的函式的引數
- daemon:是否為守護執行緒
相關屬性與函式:
- start():啟動執行緒,只能呼叫一次;
- run():執行緒執行的操作,可繼承Thread重寫,引數可從args和kwargs獲取;
- join([timeout]):堵塞呼叫執行緒,直到被呼叫執行緒執行結束或超時;如果 沒設定超時時間會一直堵塞到被呼叫執行緒結束。
- name/getName():獲得執行緒名;
- setName():設定執行緒名;
- ident:執行緒是已經啟動,未啟動會返回一個非零整數;
- is_alive():判斷是否在執行,啟動後,終止前;
- daemon/isDaemon():執行緒是否為守護執行緒;
- setDaemon():設定執行緒為守護執行緒;
3.Lock(指令鎖)與RLock(可重入鎖)
上節就說過了,多個執行緒併發地去訪問臨界資源可能會引起執行緒同步 安全問題,這裡寫個簡單的例子,多執行緒寫入同一個檔案:

開啟test.txt,發現結果並沒有按照我們預想的1-20那樣順序列印,而是亂的。

在threading模組中提供了兩個類來確保多執行緒共享資源的訪問: Lock 和 RLock
Lock
:指令鎖,有兩種狀態(鎖定與非鎖定),以及兩個基本函式:
使用**acquire
()設定為locked狀態,使用release
()設定為unlocked**狀態。
acquire()有兩個可選引數:blocking=True:是否堵塞當前執行緒等待;
timeout=None:堵塞等待時間。如果成功獲得lock,acquire返回True,
否則返回False,超時也是返回False。
使用起來也很簡單,在訪問共享資源的地方acquire一下,用完release就好:

這裡把迴圈次數改成了100,test.txt中寫入順序也是正確的,有效~ 另外需要注意:如果鎖的狀態是unlocked,此時呼叫release會 丟擲RuntimeError異常!
RLock
:可重入鎖,和Lock類似,但RLock卻可以被同一個執行緒請求多次!
比如在一個執行緒裡呼叫Lock物件的acquire方法兩次:

你會發現程式卡住不動,因為已經發生了死鎖...但是在都在同一個主執行緒裡, 這樣不就很搞笑嗎?這個時候就可以引入RLock了,使用RLock編寫一樣程式碼:


並沒有出現Lock那樣死鎖的情況,但是要注意使用RLock,acquire與release需要 成對出現,就是有多少個acquire,就要有多少個release,才能真正釋放鎖!
有點意思,點進去看看原始碼是怎麼實現的,顯示acquire方法:

如果呼叫acquire方法是同一執行緒的話,計數器_count加1;在看下release:

哈哈,一樣的套路,_count減1。
小結
本節我們開始來啃Python併發裡的threading,在學習執行緒區域性變數的時候, 順道把模組原始碼擼了一遍,而且還Get了很多以前沒學過的東西,開森, 本節要消化的內容已經挺多的了,就先寫那麼多吧~

參考文獻:
來啊,Py交易啊
想加群一起學習Py的可以加下,智障機器人小Pig,驗證資訊裡包含: Python,python,py,Py,加群,交易,屁眼 中的一個關鍵詞即可通過;

驗證通過後回覆 加群 即可獲得加群連結(不要把機器人玩壞了!!!)~~~ 歡迎各種像我一樣的Py初學者,Py大神加入,一起愉快地交流學♂習,van♂轉py。
