小豬的Python學習之旅 —— 6.捋一捋Python線程概念

引言html

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

沒佔滿的帶寬(10M帶寬),1%的CPU佔用率(筆者的是i7 6700K),要不要 那麼浪費,因此,不搞點多線程,多進程,協程這樣的東西提升下資源利用 率,怎麼也說不過去吧?然而關於線程這種話題,通常都是會讓不少新手 玩家望而卻步,並且據說Python裏還有什麼全局解釋器鎖(GIL),搞得Py沒法 實現高效的多線程,一聽就感受很難:git

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


1.程序,進程,線程,多線程,多進程

多線程與多進程的理解算法

操做系統原理相關的書,基本都會提到一句很經典的話: "進程是資源分配的最小單位,線程則是CPU調度的最小單位"。數據庫

說到進程,若是你是windows的電腦的話,Ctrl+Alt+Del打開任務 管理器,能夠看到當前電腦上正在運行的不少個進程,網易雲啊, QQ,微信啊,等等;這就是多進程,只是每一個進程各司其職完成 對應的功能而已,播放、聊天,互不干擾。這是吃瓜羣衆的見解, 而對於咱們開發來講,多進程的概念更傾向於:多個進程協同地去完成 同一項工做,爲何要在應用裏使用多線程,我的的見解以下: 爲了擺脫系統的一些限制和爲本身的應用獲取更多的資源,舉個例子: 在Android中爲每一個應用(進程)限制類最大內存,單個進程超過這個 閥值是會OOM的,而使用多進程技術能夠減小內存溢出的問題; 再舉個例子:Python在實現Python解析器(CPython)時引入GIL鎖 這種東西,使得任什麼時候候僅有一個線程在執行,多線程的效率還 可能比不上單線程,使用多線程能夠規避這個限制。編程

說完多進程,而後說下多線程,首先爲什麼會引入線程呢?舉個例子: 你有一個文本程序,接收用戶的輸入,顯示到屏幕上,並保存到硬盤裏, 由三個進程組成:輸入接收進程A,顯示內容進程B,寫入硬盤進程C, 而他們之間共同須要要擁有的東西——文本內容,由於進程A,B,C 運行在不一樣的內存空間,這就涉及到進程通訊問題了,而頻繁的切換 勢必致使性能上的損失。有沒有一種機制使得作這三個任務時共享資源呢? 這個時候線程(輕量級的進程)就粉墨登場啦!感受就像進程又開闢了 一個小世界同樣:系統 -> 進程 -> 線程,系統裏有不少進程,進程裏 又有不少線程。(有點像鬥破小說那種套路...)windows

相信到這裏你對多進程和多線程的概念就應一清二楚了,簡單比較下 二者的區別與使用場景吧:(摘自:淺談多進程多線程的選擇)緩存

對比維度 多進程 多線程
數據共享、同步 數據共享複雜,須要用IPC;
數據是分開的,同步簡單
共享進程數據,數據共享簡單,
但也是由於這個緣由致使同步複雜
內存、CPU 佔用內存多,切換複雜,CPU利用率低 佔用內存少,切換簡單,CPU利用率高
建立銷燬、切換 建立銷燬、切換複雜,速度慢 建立銷燬、切換簡單,速度很快
編程、調試 編程簡單,調試簡單 編程複雜,調試複雜
可靠性 進程間不會互相影響 一個線程掛掉將致使整個進程掛掉
分佈式 適應於多核、多機分佈式;若是一臺
機器不夠,擴展到多臺機器比較簡單
適應於多核分佈式

2.線程的生命週期

各個狀態說明:安全

  • 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兼容;

小結

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


參考文獻


來啊,Py交易啊

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

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

相關文章
相關標籤/搜索