小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

coder-pig發表於2019-02-26

引言

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

另外,在查閱相關資料的時候發現很多文章還是用的 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(物件,屬性)**判斷物件中
是否有該屬性!

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

輸出結果

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

厲害了,不同執行緒訪問果然是返回的不同值,小豬這種求知慾
旺盛的人肯定是要扒一波看看是怎麼實現的啦,跟原始碼會比較
枯燥,先簡單說下實現套路:

threading.local()例項化一個全域性物件,這個全域性物件裡有
一個大字典,鍵值為兩個弱引用物件 {執行緒物件,字典物件},
然後可以通過current_thread()獲得當前的執行緒物件,然後根據
這個物件可以拿到對應的
字典物件
,然後進行引數的讀或者寫。

是的大概套路就是這樣,接下來就是剖析原始碼環節了,挺枯燥的,
可以不看,看的話,相信你會收穫非常多,小豬昨天下午開始看
_threading_local.py這個模組的原始碼,僅僅246行,卻看到了晚上
十點才捨得回家,收益頗豐,Get了N多知識點,至少在那些什麼
Python教程裡沒看到過,每弄懂一個都會忍不出發出:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

*3._threading_local原始碼解析

按住ctrl點local()方法,會進到threading.py模組,會定位到這一行:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

關注點在**_localimpllocal**兩個類上,我們先把這個模組的原始碼
全選,然後新建一個Python檔案,把內容貼上到裡面,為什麼要
這樣做呢?

:因為這樣方便我們進行程式碼執行跟蹤啊,Debug除錯
或打Log跟蹤方法執行順序,或者檢視某個時刻某些變數的值!

很多小夥伴可能只會print不會使用Debug除錯,這裡順道簡單
介紹下怎麼用,掌握這個對跟原始碼非常有用,務必掌握!!!

1.PyCharm除錯速成

點選左側邊欄可以下斷點,在除錯模式下執行的話,執行到
這一行的時候會暫時掛起,並啟用偵錯程式視窗:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

執行到我們埋下斷點的這一行後,就會掛起並啟用下面這個
偵錯程式視窗:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

單步除錯

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

Step Over(F8),程式向下執行一行,如果該行
函式被呼叫,直接執行完返回,然後執行下一行;

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

Step Into(F7)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

Step Into My Code(Alt + Shift + F7)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

Step Out(Shift + F8)
跳出這個函式,返回該函式被呼叫處的下一行語句。

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

Run to Cursor(Alt + F9)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

,直接跳過當前斷點,
進入下一個斷點。

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

,然後
輸入你想監視的變數名,如果名字太長或者懶,可以直接右鍵
變數,Add To Watches即可!不想監視時可右鍵Remove Watch

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

斷點設定,點選左側:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

,可以開啟斷點設定視窗,可以在此
看到所有的斷點,設定條件斷點(滿足某個條件時,暫停程式執行),
刪除斷點,或者臨時禁用斷點等。

好的,關於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()**物件:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

又觸發新知識點:黑魔法__slots__


4.Python黑魔法__slots__內建屬性

作用是阻止在例項化類時為例項分配dict,使用這個東西會帶來:
更快的屬性訪問速度減少記憶體消耗。此話怎麼說?

預設情況下,Python的類例項都會有一個**dict來儲存例項的屬性,
注意:只儲存例項的變數,不會儲存類屬性!!!
可以呼叫內建屬性
dict**進行訪問,比如下面的例子:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

輸出結果

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

輸出結果

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

瞭解完**slots後,我們回到我們的原始碼,回到_localimpl的init()**

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

接著回到local類的**new()** 函式,這裡是一個設定屬性的方法:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

簡單點理解就是為local設定了一個**_localimpl物件,後面
可以根據根據這個name = _local__impl拿到對應的
_localimpl**物件!

而且這裡沒那麼簡單,local類裡對這個函式進行了重寫:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

這裡前面判斷name是否為__dict__,猜測是許可權控制,不允許
外部通過**setattrdelattr**來操作字典,只允許通過
**_patch()**方法來修改操作字典!

接著繼續來跟下**_patch()**方法:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

@contextmanager 又是什麼東西???

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

又是新的知識點~


5.@contextmanager

這就涉及到我們以前學習的with結構了,在爬蟲寫入檔案那裡用過,
不用自己寫finally,然後在裡面去close()檔案,以避免不必要的錯誤,
不知道你還記不記得,不記得的話回頭翻翻吧。

對於類似於檔案關閉這種不想遺忘的重要操作,我們可以自己封裝
一個with結構來進行處理,封裝也很簡單,再定義你那個類的時候
重寫**enter方法和exit**方法,比如檔案關閉那個可以自定義
成這樣的:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

然後又是新的知識點:Python弱引用函式ref()


6.Python弱引用函式ref()

ref()這個函式是weakref模組 提供的用於建立一個弱引用的函式,
引數異常是想建立弱引用的物件當弱引用的物件被刪除後的回撥函式
為什麼要用弱引用?

Python和其他高階語言一樣,使用垃圾回收器來自動銷燬不再使用的物件,
每個物件都有一個引用計數,當這個計數為0時,Python才能夠安全地銷燬
這個物件,當物件只剩下弱引用時也會回收!

這裡的local_deleted()thread_deleted() 這兩個回撥引數
就是在**_localimpl物件執行緒物件**被回收時觸發:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

localimpl物件被回收時把執行緒裡持有localimpl物件的弱引用刪除掉,
執行緒物件物件被回收時,彈出大字典中該執行緒對應的資料字典;

剩下的三句就是儲存_localimpl物件的弱引用到thread的**dict裡,
localimpl物件新增鍵值對
(執行緒弱引用,執行緒對應的資料字典)
大字典中,然後
返回執行緒對應的資料字典**。

又回到**_patch()方法,拿到引數,然後又呼叫init函式
然後呼叫了
init函式,這裡不是很明白動機,猜測是如果
另外重寫了local的
init**函式,可以呼叫一些其他的操作吧。

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

最後yield返回一個生成器物件。

到此,_threading_local模組的完整的原始碼實現套路就浮出水面了,
不錯,Get了很多新的姿勢,如果你還有些疑惑的話,可以自己Debug,
跟跟方法的呼叫順序,慢慢體會。


4.執行緒物件(threading.Thread)

使用threading.Thread建立執行緒

可以通過下面兩種方法建立新執行緒:

  • 1.直接建立threading.Thread物件,並把呼叫物件作為引數傳入
  • 2.繼承threading.Thread類,**重寫run()**方法;

這裡寫程式碼測試個東西:到底使用多執行緒快還是單執行緒快~

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

兩次執行結果採集:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

Thread類建構函式

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

引數依次是

  • 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(可重入鎖)

上節就說過了,多個執行緒併發地去訪問臨界資源可能會引起執行緒同步
安全問題,這裡寫個簡單的例子,多執行緒寫入同一個檔案

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

threading模組中提供了兩個類來確保多執行緒共享資源的訪問:
LockRLock

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

這裡把迴圈次數改成了100,test.txt中寫入順序也是正確的,有效~
另外需要注意:如果鎖的狀態是unlocked,此時呼叫release會
丟擲RuntimeError異常!

RLock可重入鎖,和Lock類似,但RLock卻可以被同一個執行緒請求多次
比如在一個執行緒裡呼叫Lock物件的acquire方法兩次:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

     輸出結果:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

並沒有出現Lock那樣死鎖的情況,但是要注意使用RLockacquire與release需要
成對出現,就是有多少個acquire,就要有多少個release,才能真正釋放鎖!

有點意思,點進去看看原始碼是怎麼實現的,顯示acquire方法:

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

哈哈,一樣的套路,_count減1。


小結

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

參考文獻


來啊,Py交易啊

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

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

小豬的Python學習之旅 —— 7.Python併發之threading模組(1)

相關文章