內容簡述:
線程與進程的相關概念
- 1、程序,進程,線程,多進程,多線程
- 2、線程的生命周期
- 3、并行與并發,同步與異步
- 4、線程同步安全
- 5、與鎖有關的特殊情況:死鎖,饑餓與活鎖
- 6、守護線程
- 7、線程并發的經典問題:生產中與消費者問題
- 8、Python中的GIL鎖
- 9、Python中對多線程與多進程的支持
線程與進程的相關概念
關于線程和進程的話題,大部分的書只是微微提下,讀者學完云里霧里,不知所以。本章會對Python中的多線程和多進程進行詳解。大部分都是概念性的東西,不要去死記硬背,學完了解有個大概印象就好。
1、程序,進程,線程,多進程,多線程
關于程序,進程和線程的一些名詞概念如圖所示:
[圖片上傳失敗...(image-85ba8e-1555409468002)]
有句非常經典的話:“進程是資源分配的最小單位,線程則是CPU調度的最小單位”。
先說說「多進程」:從普通用戶的視角:
如果你的電腦是Windows的話,Ctrl+Alt+Del打開任務管理器,可以看到電腦運行著很多的進程,比如QQ,微信,網易云音樂等。這就是多進程,每個進程各司其職完成對應的功能,互不干擾,你聊天的時候音樂照常播放。
再說說開發仔的視角:
多進程的概念更傾向于:多個進程協同地區完成同一項工作。
問題:為什么要在應用里使用多進程?
筆者觀點:擺脫系統的一些限制和為自己的應用獲取更多的資源,舉個例子:
在Android系統中會為每個應用(進程)限制最大內容,單個進程超過這個閾值會引起OOM,而使用多進程技術可以規避這個內存溢出的問題。再舉個例子:Python在實現Python解析器(CPython)時引入了GIL鎖,使得任何時候僅有
一個線程在執行,多線程的效率可能還比不上單線程,使用多進程技術可以
規避這個限制。
再說說「多線程」,首先為什么會引入線程的概念呢?舉個例子:
你有一個文本程序,三個功能組成部分:接收用戶的輸入,顯示到屏幕上,保存到硬盤里,如果由三個進程組成:輸入接收進程A,顯示內容進程B,寫入硬盤進程C,而他們之間共同需要擁有的東西——文本內容,而進程A,B,C運行在不同的內存空間,這就涉及到
進程通信問題
了,而頻繁
的進程切換勢必導致性能上的損失。有沒有一種機制使得做這三個任務時共享資源呢?這個時候線程(輕量級的進程)就派上用場了,多個線程共享進程數據。相信讀者看到這里,對于多進程和多線程
的概念應該有個初步的了解了,接下來簡單比較下兩者的優劣和使用場景:
簡書
2、線程的生命周期
線程的生命周期如圖所示:
[圖片上傳失敗...(image-bc721e-1555409468002)]
各個狀態說明:
- New(新建),新創建的線程進過初始化,進入Runnable(就緒)狀態。
- Runnable(就緒),等待線程調度,調度后進入Running(運行)狀態。
-
Running(運行),線程正常運行,期間可能會因為某些情況進入Blocked
(同步鎖;調用了sleep()和join()方法進入Sleeping狀態;執行wait()方法進入
Waiting狀態,等待其他線程notify通知喚醒)。 - Blocked(堵塞),線程暫停運行,解除堵塞后進入Runnable(就緒)狀態重新等待調度。
- Dead(死亡):線程完成了它的任務正常結束或因異常導致終止。
3、并行與并發,同步與異步
并行與并發的區別:
并行是同時處理多個任務,而并發則是處理多個任務,而不一定要同時,并行可以說是并發的子集。
同步與異步的區別:
- 同步:線程執行某個請求,如果該請求需要一段時間才能返回信息,那么這個線程
會一直等待,直到收到返回信息才能繼續執行下去。- 異步:線程執行完某個請求,不需要一直等,直接繼續執行后續操作,當有消息
返回時系統會通知線程進程處理,這樣可以提高執行的效率;異步在網絡請求
的應用非常常見。
4、線程同步安全
什么是線程同步安全問題?
當有兩個或以上線程在同一時刻訪問同一資源,可能會帶來一些問題。
比如:數據庫表不允許插入重復數據,而線程1,2都得到了數據X,然后線程1,2同時查詢了數據庫,發現沒有數據X,接著兩線程都往數據庫中插入了X,然后就出現異常了,這就是線程的同步安全問題,而這里的數據庫資源我們又稱為:臨界資源(共享資源)。
如何解決同步安全問題(同步鎖)?
當多個線程訪問臨界資源的時候,有可能會出現線程安全問題;而基本所有并發模式在解決線程安全問題時都采用"系列化訪問臨界資源"的方式,就是同一時刻,只能有一個線程訪問臨界資源,也稱"同步互斥訪問"。通常的操作就是加鎖(同步鎖),當有線程訪問臨界資源時需要獲得這個鎖,其他線程無法訪問,只能等待(堵塞),等這個線程使用完釋放鎖,供其他線程繼續訪問。
5、與鎖有關的特殊情況:死鎖,饑餓與活鎖
有了同步鎖并不意味著就一了百了了,當多個進程/線程的操作涉及到了多個鎖,
就可能出現下述三種情況:
- 死鎖(DeadLock)
兩個或以上進程(線程)在執行過程中,因爭奪資源而造成的一種互相等待的現象,如果無外力作用,他們將繼續這樣僵持下去。舉個形象化的例子:
開一個門需要兩條鑰匙,而兩個人手上各持有一條,然后都不愿意把自己的鑰匙給對方,就一直那樣僵持著,這種狀態就叫死鎖。
死鎖發生的條件:
- 互斥條件(臨界資源);
- 請求和保持條件(請求資源但不釋放自己暫用的資源);
- 不剝奪條件(線程獲得的資源只有線程使用完后自己釋放,不能被其他線程剝奪);
- 環路等待條件:在死鎖發生時,必然存在一個“進程-資源環形鏈”,t1等t2,t2等t1;
如何避免死鎖:
破壞四個條件中的一個或多個條件,常見的預防方法有如下兩種:
① 有序資源分配法:資源按某種規則統一編號,申請時必須按照升序申請: 屬于同一類的資源要一次申請完,申請不同類資源按照一定的順序申請。
② 銀行家算法:就是檢查申請者對資源的最大需求量,如果當前各類資源都可以滿足的 申請者的請求,就滿足申請者的請求,這樣申請者就可很快完成其計算,然后釋放它占用 的資源,從而保證了系統中的所有進程都能完成,所以可避免死鎖的發生。 理論上能夠非常有效的避免死鎖,但從某種意義上說,缺乏使用價值,因為很少有進程能夠知道所需資源的最大值,而且進程數目也不是固定的,往往是不斷變化的, 況且原本可用的資源也可能突然間變得不可用(比如打印機損壞)。
- 2.饑餓(starvation)與餓死(starve to death)
資源分配策略有可能是不公平的,即不能保證等待時間上界的存在,即使沒有發生死鎖, 某些進程可能因長時間的等待,對進程推進與相應帶來明顯影響,此時的進程就是 發生了進程饑餓(starvation),當饑餓達到一定程度即使此時進程即使完成了任務也沒有實際意義時,此時稱該進程被餓死(starve to death),舉個典型的例子:文件打印采用短文件優先策略,如果短文件太多,長文件會一直推遲,那還打印個毛。
- 3.活鎖(LiveLock)
特殊的饑餓,一系列進程輪詢等待某個不可能為真的條件為真,此時進程不會進入blocked狀態,但會占用CPU資源,活鎖還有幾率能自己解開,而死鎖則無法自己解開。(例子:都覺得對方優先級比自己高,相互謙讓,導致無法使用某資源),簡單避免活鎖的方法:先來先服務策略。
6、守護線程
又稱「后臺線程」,是一種為其他線程提供服務的線程,比如一個簡單的例子:你有兩個線程在協同的做一件事,如果有一個線程死掉,事情就無法繼續下去,此時可以引入守護線程,輪詢地去判斷兩個線程是否活著(調isAlive()),如果死掉就start開啟線程,在Python中可以在線程初始化的時候調用
setDaemon(True)
把線程設置為守護線程,另外如果程序中只剩下守護線程的話程序會自動退出。
7、線程并發的經典問題:生產中與消費者問題
說到線程并發,不得不說的一個經典問題就是:生產中與消費者問題:
兩個共享固定緩沖區大小的線程,生產者線程負責生產一定量的數據 放入緩沖區, 而消費者線程則負責消耗緩沖區中的數據,關鍵問題是需要保證兩點:
- 緩沖區滿的時候,生產者不再往緩沖區中填充數據。
- 緩存區空的時候,消費者不在消耗緩沖區中的數據。
8、Python中的GIL鎖
上面講到Python在實現Python解析器(CPython)時引入了GIL鎖,使得「任何時候僅有 一個線程在執行」,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鎖搶奪的問題,但是也增加程序實現線程間數據通訊和同步時的成本,這個需要自行進行權衡。
9、Python中對多線程與多進程的支持
Python與線程,進程相關的官方文檔鏈接:docs.python.org/3/library/c…
簡單說下這些模塊都是干嘛的:
- threading—— 提供線程相關的操作。
- multiprocessing—— 提供進程程相關的操作。
- concurrent.futures—— 異步并發模塊,實現多線程和多進程的異步并發(3.2后引入)。
- subprocess—— 創建子進程,并提供鏈接到他們輸入/輸出/錯誤管道的方法,并獲得他們的返回碼,該模塊旨在替換幾個較舊的模塊和功能:os.system與os.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兼容。