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

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

標籤: Python


引言

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

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

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


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

多線程與多進程的理解

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

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

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

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

對比維度 多進程 多線程
數據共享、同步 數據共享複雜,需要用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
https://docs.python.org/3/library/concurrency.html

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

  • 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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章