引言:
從本節開始的連續幾節我們都會圍繞著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是變數除錯視窗,可以檢視此時的變數情況!
接著就來一一說下一些除錯技巧吧:
單步除錯,
,Step Over(F8),程式向下執行一行,如果該行
函式被呼叫,直接執行完返回,然後執行下一行;
當單步除錯執行到某一個函式,如果你不想直接執行完,切到下
一行而是想看進去這個函式的執行過程的話,可以點選
Step Into(F7) ;
上面這一步,遇到官方類庫的函式也會進去,如果只想在碰到
自己定義函式才進去的話,可以點選
Step Into My Code(Alt + Shift + F7)
進入函式後確定沒什麼問題了,可以點選
Step Out(Shift + F8)
跳出這個函式,返回該函式被呼叫處的下一行語句。
如果想快速執行到下一個斷點的位置,可以點選
Run to Cursor(Alt + F9)
跨斷點除錯,點選左側欄的:
,直接跳過當前斷點,
進入下一個斷點。
監視變數,有時右側Variables,顯示的變數有很多時,而你
想關注某一個變數而已,可以點選這個小眼鏡:
,然後
輸入你想監視的變數名,如果名字太長或者懶,可以直接右鍵
變數,Add To Watches即可!不想監視時可右鍵Remove Watch。
停止除錯,點選左側紅色按鈕即可跳過除錯,不是停止程式!:
斷點設定,點選左側:
,可以開啟斷點設定視窗,可以在此
看到所有的斷點,設定條件斷點(滿足某個條件時,暫停程式執行),
刪除斷點,或者臨時禁用斷點等。
好的,關於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。