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

引言html

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

另外,在查閱相關資料的時候發現不少文章仍是用的 thread模塊, 在高版本中已經使用threading來替代thread了!!!若是你在 Python 2.x版本想使用threading的話,可使用dummy_threading 話很少說開始本節內容~python


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)

先說個知識點:segmentfault

在一個進程內全部的線程共享進程的全局變量,線程間共享數據很方便 可是每一個線程均可以隨意修改全局變量,可能會引發線程安全問題, 這個時候,能夠對全局變量進行加鎖來解決。對於線程私有數據能夠 經過使用局部變量,只有線程自身能夠訪問,其餘線程沒法訪問, 除此以外,Python還給咱們提供了ThreadLocal變量,自己是一個全局 變量,可是線程們卻可使用它來保存私有數據安全

用法也很簡單,定義一個全局變量:data = thread.local(),而後就能夠 往裏面存數據啦,好比data.num = xxx,寫個簡單例子來驗證下: :若是data沒有設置對應的屬性,直接取會報AttributeError異常, 使用時能夠捕獲這個異常,或者先調用**hasattr(對象,屬性)**判斷對象中 是否有該屬性!多線程

輸出結果併發

厲害了,不一樣線程訪問果真是返回的不一樣值,小豬這種求知慾 旺盛的人確定是要扒一波看看是怎麼實現的啦,跟源碼會比較 枯燥,先簡單說下實現套路:ide

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看看代碼結構

關注點在**_localimpllocal**兩個類上,咱們先把這個模塊的源碼 全選,而後新建一個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__,猜想是權限控制,不容許 外部經過**setattrdelattr**來操做字典,只容許經過 **_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 類裏重寫了**enterexit**函數。

最後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模塊中提供了兩個類來確保多線程共享資源的訪問: LockRLock

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那樣死鎖的狀況,可是要注意使用RLockacquire與release須要 成對出現,就是有多少個acquire,就要有多少個release,才能真正釋放鎖!

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

若是調用acquire方法是同一線程的話,計數器_count加1;在看下release:

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


小結

本節咱們開始來啃Python併發裏的threading,在學習線程局部變量的時候, 順道把模塊源碼擼了一遍,並且還Get了不少之前沒學過的東西,開森, 本節要消化的內容已經挺多的了,就先寫那麼多吧~


參考文獻


來啊,Py交易啊

想加羣一塊兒學習Py的能夠加下,智障機器人小Pig,驗證信息裏包含: PythonpythonpyPy加羣交易屁眼 中的一個關鍵詞便可經過;

驗證經過後回覆 加羣 便可得到加羣連接(不要把機器人玩壞了!!!)~~~ 歡迎各類像我同樣的Py初學者,Py大神加入,一塊兒愉快地交流學♂習,van♂轉py。

相關文章
相關標籤/搜索