豬行天下之Python基礎——9.1 Python多線程與多進程(上)

內容簡述:

線程與進程的相關概念
 html

  • 一、程序,進程,線程,多進程,多線程
  • 二、線程的生命週期
  • 三、並行與併發,同步與異步
  • 四、線程同步安全
  • 五、與鎖有關的特殊狀況:死鎖,飢餓與活鎖
  • 六、守護線程
  • 七、線程併發的經典問題:生產中與消費者問題
  • 八、Python中的GIL鎖
  • 九、Python中對多線程與多進程的支持

線程與進程的相關概念python

關於線程和進程的話題,大部分的書只是微微提下,讀者學完雲裏霧裏,不知因此。本章會對Python中的多線程和多進程進行詳解。大部分都是概念性的東西,不要去死記硬背,學完了解有個大概印象就好。算法


一、程序,進程,線程,多進程,多線程

關於程序,進程和線程的一些名詞概念如圖所示:數據庫

有句很是經典的話:「進程是資源分配的最小單位,線程則是CPU調度的最小單位」。編程

先說說「多進程」:從普通用戶的視角:緩存

若是你的電腦是Windows的話,Ctrl+Alt+Del打開任務管理器,能夠看到電腦運行着不少的進程,好比QQ,微信,網易雲音樂等。這就是多進程,每一個進程各司其職完成對應的功能,互不干擾,你聊天的時候音樂照常播放。安全

再說說開發仔的視角:微信

多進程的概念更傾向於:多個進程協同地區完成同一項工做。網絡

問題:爲何要在應用裏使用多進程多線程

筆者觀點:擺脫系統的一些限制和爲本身的應用獲取更多的資源,舉個例子:
在Android系統中會爲每一個應用(進程)限制最大內容,單個進程超過這個閾值會引發OOM,而使用多進程技術能夠規避這個內存溢出的問題。再舉個例子:Python在實現Python解析器(CPython)時引入了GIL鎖,使得任什麼時候候僅有
一個線程在執行,多線程的效率可能還比不上單線程,使用多進程技術能夠
規避這個限制。

再說說「多線程」,首先爲何會引入線程的概念呢?舉個例子:

你有一個文本程序,三個功能組成部分:接收用戶的輸入,顯示到屏幕上,保存到硬盤裏,若是由三個進程組成:輸入接收進程A,顯示內容進程B,寫入硬盤進程C,而他們之間共同須要擁有的東西——文本內容,而進程A,B,C運行在不一樣的內存空間,這就涉及到進程通訊問題了,而頻繁
的進程切換勢必致使性能上的損失。有沒有一種機制使得作這三個任務時共享資源呢?這個時候線程(輕量級的進程)就派上用場了,多個線程共享進程數據。相信讀者看到這裏,對於多進程和多線程
的概念應該有個初步的瞭解了,接下來簡單比較下二者的優劣和使用場景:

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

二、線程的生命週期

線程的生命週期如圖所示

各個狀態說明

  • New(新建),新建立的線程進過初始化,進入Runnable(就緒)狀態。
  • Runnable(就緒),等待線程調度,調度後進入Running(運行)狀態。
  • Running(運行),線程正常運行,期間可能會由於某些狀況進入Blocked
    (同步鎖;調用了sleep()和join()方法進入Sleeping狀態;執行wait()方法進入
    Waiting狀態,等待其餘線程notify通知喚醒)。
  • Blocked(堵塞),線程暫停運行,解除堵塞後進入Runnable(就緒)狀態從新等待調度。
  • Dead(死亡):線程完成了它的任務正常結束或因異常致使終止。

三、並行與併發,同步與異步

並行與併發的區別

並行是同時處理多個任務,而併發則是處理多個任務,而不必定要同時,並行能夠說是併發的子集

同步與異步的區別

  • 同步:線程執行某個請求,若是該請求須要一段時間才能返回信息,那麼這個線程
    一直等待,直到收到返回信息才能繼續執行下去。
  • 異步:線程執行完某個請求,不須要一直等直接繼續執行後續操做,當有消息
    返回時系統會通知線程進程處理,這樣能夠提升執行的效率;異步在網絡請求
    的應用很是常見。

四、線程同步安全

什麼是線程同步安全問題

當有兩個或以上線程在同一時刻訪問同一資源,可能會帶來一些問題。
好比:數據庫表不容許插入重複數據,而線程1,2都獲得了數據X,而後線程1,2同時查詢了數據庫,發現沒有數據X,接着兩線程都往數據庫中插入了X,而後就出現異常了,這就是線程的同步安全問題,而這裏的數據庫資源咱們又稱爲:臨界資源(共享資源)

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

當多個線程訪問臨界資源的時候,有可能會出現線程安全問題;而基本全部併發模式在解決線程安全問題時都採用"系列化訪問臨界資源"的方式,就是同一時刻,只能有一個線程訪問臨界資源,也稱"同步互斥訪問"。一般的操做就是加鎖(同步鎖),當有線程訪問臨界資源時須要得到這個鎖,其餘線程沒法訪問,只能等待(堵塞),等這個線程使用完釋放鎖,供其餘線程繼續訪問。


五、與鎖有關的特殊狀況:死鎖,飢餓與活鎖

有了同步鎖並不意味着就一了百了了,當多個進程/線程的操做涉及到了多個鎖,
就可能出現下述三種狀況:

  • 死鎖(DeadLock)

兩個或以上進程(線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若是無外力做用,他們將繼續這樣僵持下去。舉個形象化的例子:

開一個門須要兩條鑰匙,而兩我的手上各持有一條,而後都不肯意把本身的鑰匙給對方,就一直那樣僵持着,這種狀態就叫死鎖。

死鎖發生的條件

  • 互斥條件(臨界資源);
  • 請求和保持條件(請求資源但不釋放本身暫用的資源);
  • 不剝奪條件(線程得到的資源只有線程使用完後本身釋放,不能被其餘線程剝奪);
  • 環路等待條件:在死鎖發生時,必然存在一個「進程-資源環形鏈」,t1等t2,t2等t1;

如何避免死鎖

破壞四個條件中的一個或多個條件,常見的預防方法有以下兩種:

① 有序資源分配法:資源按某種規則統一編號,申請時必須按照升序申請: 屬於同一類的資源要一次申請完,申請不一樣類資源按照必定的順序申請。

② 銀行家算法:就是檢查申請者對資源的最大需求量,若是當前各種資源均可以知足的 申請者的請求,就知足申請者的請求,這樣申請者就可很快完成其計算,而後釋放它佔用 的資源,從而保證了系統中的全部進程都能完成,因此可避免死鎖的發生。 理論上可以很是有效的避免死鎖,但從某種意義上說,缺少使用價值,由於不多有進程可以知道所需資源的最大值,並且進程數目也不是固定的,每每是不斷變化的, 何況本來可用的資源也可能忽然間變得不可用(好比打印機損壞)。

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

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

  • 3.活鎖(LiveLock)

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


六、守護線程

又稱「後臺線程」,是一種爲其餘線程提供服務的線程,好比一個簡單的例子:你有兩個線程在協同的作一件事,若是有一個線程死掉,事情就沒法繼續下去,此時能夠引入守護線程,輪詢地去判斷兩個線程是否活着(調isAlive()),若是死掉就start開啓線程,在Python中能夠在線程初始化的時候調用setDaemon(True)把線程設置爲守護線程,另外若是程序中只剩下守護線程的話程序會自動退出。


七、線程併發的經典問題:生產中與消費者問題

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

兩個共享固定緩衝區大小的線程,生產者線程負責生產必定量的數據 放入緩衝區, 而消費者線程則負責消耗緩衝區中的數據,關鍵問題是須要保證兩點:

  • 緩衝區滿的時候,生產者再也不往緩衝區中填充數據。
  • 緩存區空的時候,消費者不在消耗緩衝區中的數據。

八、Python中的GIL鎖

上面講到Python在實現Python解析器(CPython)時引入了GIL鎖,使得「任什麼時候候僅有 一個線程在執行」,Python多線程的效率可能還比不上單線程,那麼這個GIL鎖是什麼?

概念:全局解釋器鎖,用於同步線程的一種機制,使得任什麼時候候僅有一個線程在執行。GIL 並非Python的特性,只是在實現Python解析器(CPython)時引入的一個概念。換句話說,Python徹底能夠不依賴於GILPython解釋器進程內的多線程是以協做多任務方式執行的,當一個線程遇到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鎖搶奪的問題,可是也增長程序實現線程間數據通信和同步時的成本,這個須要自行進行權衡。


九、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兼容。
相關文章
相關標籤/搜索