小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念

coder-pig發表於2018-01-24

引言

從剛開始學習Python爬蟲的時候,就一直惦記著多執行緒這個東西, 想想每次下載圖片都是單執行緒,一個下完繼續下一個,多呆啊

小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念

小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念

沒佔滿的頻寬(10M頻寬),1%的CPU佔用率(筆者的是i7 6700K),要不要 那麼浪費,所以,不搞點多執行緒,多程式,協程這樣的東西提高下資源利用 率,怎麼也說不過去吧?然而關於執行緒這種話題,一般都是會讓很多新手 玩家望而卻步,而且聽說Python裡還有什麼全域性直譯器鎖(GIL),搞得Py無法 實現高效的多執行緒,一聽就感覺很難:

小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念

虛個卵子哦,跟著小豬把Python裡和多執行緒相關的東西都擼一遍吧! 本節主要是對一些概念進行了解~


1.程式,程式,執行緒,多執行緒,多程式

小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念

多執行緒與多程式的理解

作業系統原理相關的書,基本都會提到一句很經典的話: "程式是資源分配的最小單位,執行緒則是CPU排程的最小單位"。

說到程式,如果你是windows的電腦的話,Ctrl+Alt+Del開啟任務 管理器,可以看到當前電腦上正在執行的很多個程式,網易雲啊, QQ,微信啊,等等;這就是多程式,只是每個程式各司其職完成 對應的功能而已,播放、聊天,互不干擾。這是吃瓜群眾的看法, 而對於我們開發來說,多程式的概念更傾向於:多個程式協同地去完成 同一項工作,為什麼要在應用裡使用多執行緒,個人的看法如下: 為了擺脫系統的一些限制和為自己的應用獲取更多的資源,舉個例子: 在Android中為每個應用(程式)限制類最大記憶體,單個程式超過這個 閥值是會OOM的,而使用多程式技術可以減少記憶體溢位的問題; 再舉個例子:Python在實現Python解析器(CPython)時引入GIL鎖 這種東西,使得任何時候僅有一個執行緒在執行,多執行緒的效率還 可能比不上單執行緒,使用多執行緒可以規避這個限制。

說完多程式,然後說下多執行緒,首先為何會引入執行緒呢?舉個例子: 你有一個文字程式,接收使用者的輸入,顯示到螢幕上,並儲存到硬碟裡, 由三個程式組成:輸入接收程式A,顯示內容程式B,寫入硬碟程式C, 而他們之間共同需要要擁有的東西——文字內容,因為程式A,B,C 執行在不同的記憶體空間,這就涉及到程式通訊問題了,而頻繁的切換 勢必導致效能上的損失。有沒有一種機制使得做這三個任務時共享資源呢? 這個時候執行緒(輕量級的程式)就粉墨登場啦!感覺就像程式又開闢了 一個小世界一樣:系統 -> 程式 -> 執行緒,系統裡有很多程式,程式裡 又有很多執行緒。(有點像鬥破小說那種套路...)

相信到這裡你對多程式和多執行緒的概念就應一清二楚了,簡單比較下 兩者的區別與使用場景吧:(摘自:淺談多程式多執行緒的選擇)

對比維度 多程式 多執行緒
資料共享、同步 資料共享複雜,需要用IPC;
資料是分開的,同步簡單
共享程式資料,資料共享簡單,
但也是因為這個原因導致同步複雜
記憶體、CPU 佔用記憶體多,切換複雜,CPU利用率低 佔用記憶體少,切換簡單,CPU利用率高
建立銷燬、切換 建立銷燬、切換複雜,速度慢 建立銷燬、切換簡單,速度很快
程式設計、除錯 程式設計簡單,除錯簡單 程式設計複雜,除錯複雜
可靠性 程式間不會互相影響 一個執行緒掛掉將導致整個程式掛掉
分散式 適應於多核、多機分散式;如果一臺
機器不夠,擴充套件到多臺機器比較簡單
適應於多核分散式

2.執行緒的生命週期

小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念

各個狀態說明:

  • 1.New(新建),新建立的執行緒進過初始化,進入**Runnable(就緒)**狀態;
  • 2.Runnable(就緒),等待執行緒排程,排程後進入**Running(執行)**狀態;
  • 3.Running(執行),執行緒正常執行,期間可能會因為某些情況進入Blocked(堵塞) 狀態(同步鎖;呼叫了sleep()和join()方法進入Sleeping狀態;執行wait() 方法進入Waiting狀態,等待其他執行緒notify通知喚醒);
  • 4.Blocked(堵塞),執行緒暫停執行,解除堵塞後進入**Runnable(就緒)**狀態 重新等待排程;
  • 5.Dead(死亡):執行緒完成了它的任務正常結束或因異常導致終止;

3.並行與併發

並行是同時處理多個任務,而併發則是處理多個任務,而不一定要同時, 並行可以說是併發的子集。


4.同步與非同步

同步:執行緒執行某個請求,如果該請求需要一段時間才能返回資訊, 那麼這個執行緒會一直等待,直到收到返回資訊才能繼續執行下去;

非同步:執行緒執行完某個請求,不需要一直等,直接繼續執行後續操作, 當有訊息返回時系統會通知執行緒程式處理,這樣可以提高執行的效率; 非同步在網路請求的應用非常常見~


5.執行緒同步安全問題

當有兩個或以上執行緒在同一時刻訪問同一資源,可能會帶來一些問題, 比如:資料庫表不允許插入重複資料,而執行緒1,2都得到了資料X,然後 執行緒1,2同時查詢了資料庫,發現沒有資料X,接著兩執行緒都往資料庫中 插入了X,然後就GG啦,這就是執行緒的同步安全問題,而這裡的資料庫 資源我們又稱為:臨界資源(共享資源)


6.如何解決同步安全問題(同步鎖)

當多個執行緒訪問臨界資源的時候,有可能會出現執行緒安全問題; 而基本所有併發模式在解決執行緒安全問題時都採用"系列化訪問 臨界資源"的方式,就是同一時刻,只能有一個執行緒訪問臨界資源, 也稱"同步互斥訪問"。通常的操作就是加鎖(同步鎖),當有執行緒訪問 臨界資源時需要獲得這個鎖,其他執行緒無法訪問,只能等待(堵塞), 等這個執行緒使用完釋放鎖,供其他執行緒繼續訪問。


7.與鎖有關的特殊情況:死鎖,飢餓與活鎖

有了同步鎖不意味著就一了百了了,當多個程式/執行緒的操作涉及到了多個鎖, 就可能出現下述三種情況:

  • 死鎖(DeadLock)

兩個或以上程式(執行緒)在執行過程中,因爭奪資源而造成的一種互相等待的現象, 如果無外力作用,他們將繼續這樣僵持下去;簡單點說:兩個人互相持有對方想要的資源, 然後每一方都不願意放棄自己手上的資源,就一直那樣僵持著。

死鎖發生的條件

互斥條件(臨界資源); 請求和保持條件(請求資源但不釋放自己暫用的資源); 不剝奪條件(執行緒獲得的資源只有執行緒使用完後自己釋放,不能被其他執行緒剝奪); 環路等待條件:在死鎖發生時,必然存在一個”程式-資源環形鏈”,t1等t2,t2等t1;

如何避免死鎖

破壞四個條件中的一個或多個條件,常見的預防方法有如下兩種: 有序資源分配法:資源按某種規則統一編號,申請時必須按照升序申請: 1.屬於同一類的資源要一次申請完;2.申請不同類資源按照一定的順序申請。 銀行家演算法:就是檢查申請者對資源的最大需求量,如果當前各類資源都可以滿足的 申請者的請求,就滿足申請者的請求,這樣申請者就可很快完成其計算,然後釋放它佔用 的資源,從而保證了系統中的所有程式都能完成,所以可避免死鎖的發生。 理論上能夠非常有效的避免死鎖,但從某種意義上說,缺乏使用價值,因為很少有程式 能夠知道所需資源的最大值,而且程式數目也不是固定的,往往是不斷變化的, 況且原本可用的資源也可能突然間變得不可用(比如印表機損壞)。

  • 飢餓(starvation)與餓死(starve to death)

資源分配策略有可能是不公平的,即不能保證等待時間上界的存在,即使沒有 發生死鎖, 某些程式可能因長時間的等待,對程式推進與相應帶來明顯影響, 此時的程式就是 發生了程式飢餓(starvation),當飢餓達到一定程式即此時 程式即使完成了任務也 沒有實際意義時,此時稱該程式被餓死(starve to death), 典型的例子: 檔案列印,採用短檔案優先策略,如果短檔案太多,長檔案會一直 推遲,那還列印個毛。

  • 活鎖(LiveLock)

特殊的飢餓,一系列程式輪詢等待某個不可能為真的條件為真,此時程式不會 進入blocked狀態, 但會佔用CPU資源,活鎖還有機率能自己解開,而死鎖則 無法自己解開。(例子:都覺得對方優先順序比自己高,相互謙讓,導致無法 使用某資源),簡單避免死鎖的方法:先來先服務策略。


8.守護執行緒

也叫後臺執行緒,是一種為其他執行緒提供服務的執行緒,比如一個簡單的例子: 你有兩個執行緒在協同的做一件事,如果有一個執行緒死掉,事情就無法繼續 下去,此時可以引入守護執行緒,輪詢地去判斷兩個執行緒是否或者(調isAlive()), 如果死掉就start開啟執行緒,在Python中可以線上程初始化的時候呼叫 setDaemon(True)把執行緒設定為守護執行緒,如果程式中只剩下守護執行緒 的話會自動退出


9.執行緒併發的經典問題:生產中與消費者問題

說到執行緒併發,不得不說的一個經典問題就是:生產中與消費者問題:

兩個共享固定緩衝區大小的執行緒,生產者執行緒負責生產一定量的資料 放入緩衝區, 而消費者執行緒則負責消耗緩衝區中的資料,關鍵問題是 需要保證兩點:

  • 1.緩衝區滿的時候,生產者不再往緩衝區中填充資料
  • 2.快取區空的時候,消費者不在消耗緩衝區中的資料

聽不懂也沒什麼,這個後面會寫例子的~


10.Python中的GIL鎖

概念

全域性直譯器鎖,用於同步執行緒的一種機制,使得任何時候僅有一個執行緒在執行。 GIL 並不是Python的特性,只是在實現Python解析器(CPython)時引入的 一個概念。換句話說,Python完全可以不依賴於GIL。

Python直譯器程式內的多執行緒是以協作多工方式執行的,當一個執行緒遇到 I/O操作時會釋放GIL。而依賴CPU計算的執行緒則是執行程式碼量到一定的閥值, 才會釋放GIL。而在Python 3.2開始使用新的GIL,使用固定的超時時間來指示 當前執行緒放棄全域性鎖,就是:當前執行緒持有這個鎖,且其他執行緒請求這個鎖時, 當前執行緒就會再5毫秒後被強制釋放掉該鎖。

多執行緒在處理CPU密集型操作因為各種迴圈處理計數等,會很快達到閥值, 而多個執行緒來回切換是會消耗資源的,所以多執行緒的效率往往可能還比不上 單執行緒!而在多核CPU上效率會更低,因為多核環境下,持有鎖的CPU釋放鎖後, 其他CPU上的執行緒都會進行競爭,但GIL可能馬上又會被之前的CPU拿到拿到, 導致其他幾個CPU上被喚醒後的執行緒會醒著等待到切換時間後又進入待排程 狀態,從而造成執行緒顛簸(thrashing),導致效率更低。

問題

因為GIL鎖的原因,對於CPU密集型操作,Python多執行緒就是雞肋了?

答:是的!儘管多執行緒開銷小,但卻無法利用多核優勢! 可以使用多程式來規避這個問題,Python提供了multiprocessing 這個跨平臺的模組來幫助我們實現多程式程式碼的編寫。 每個執行緒都有自己獨立的GIL,因此不會出現程式間GIL 鎖搶奪的問題,但是也增加程式實現執行緒間資料通訊和同步 是的成本,這個需要自行進行權衡。


11.Python中對多執行緒與多程式的支援

Python與執行緒,程式相關的官方文件: 17. Concurrent Execution docs.python.org/3/library/c…

簡單介紹下里面的一些模組,後面會一個個啃~

  • threading —— 提供執行緒相關的操作
  • multiprocessing —— 提供程式程相關的操作
  • concurrent.futures —— 非同步併發模組,實現多執行緒和多程式的非同步併發(3.2後引入)
  • subprocess —— 建立子程式,並提供連結到他們輸入/輸出/錯誤管道的方法, 並獲得他們的返回碼,該模組旨在替換幾個較舊的模組和功能:os.systemos.spawn*
  • sched —— 任務排程(延時處理機制)
  • queue —— 提供同步的、執行緒安全的佇列類

還有幾個是相容模組,比如Python 2.x上用threading和Python 3.x上用thread:

  • dummy_threading:提供和threading模組相同的介面,2.x使用threading相容;
  • _thread:threading模組的基礎模組,應該儘量使用 threading 模組替代;
  • dummy_thread:提供和thread模組相同的介面,3.x使用threading相容;

小結

本節我們圍繞著執行緒以及程式相關的概念進行了解析,儘管有些 枯燥,但是如果堅持看完,相信你對於執行緒與程式的理解會更進 一步,概念什麼都是虛的,紙上得來終覺淺絕知此事要躬行, 下節開始我們來通過寫程式碼的方式一一學習這些模組吧!

小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念


參考文獻


來啊,Py交易啊

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

小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念

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

小豬的Python學習之旅 —— 6.捋一捋Python執行緒概念

相關文章