豬行天下之Python基礎——9.1 Python多執行緒與多程式(上)

coder-pig發表於2019-04-13

內容簡述:

執行緒與程式的相關概念
 

  • 1、程式,程式,執行緒,多程式,多執行緒
  • 2、執行緒的生命週期
  • 3、並行與併發,同步與非同步
  • 4、執行緒同步安全
  • 5、與鎖有關的特殊情況:死鎖,飢餓與活鎖
  • 6、守護執行緒
  • 7、執行緒併發的經典問題:生產中與消費者問題
  • 8、Python中的GIL鎖
  • 9、Python中對多執行緒與多程式的支援

執行緒與程式的相關概念

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


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

關於程式,程式和執行緒的一些名詞概念如圖所示:

有句非常經典的話:“程式是資源分配的最小單位,執行緒則是CPU排程的最小單位”。

先說說「多程式」:從普通使用者的視角:

如果你的電腦是Windows的話,Ctrl+Alt+Del開啟工作管理員,可以看到電腦執行著很多的程式,比如QQ,微信,網易雲音樂等。這就是多程式,每個程式各司其職完成對應的功能,互不干擾,你聊天的時候音樂照常播放。

再說說開發仔的視角:

多程式的概念更傾向於:多個程式協同地區完成同一項工作。

問題:為什麼要在應用裡使用多程式

筆者觀點:擺脫系統的一些限制和為自己的應用獲取更多的資源,舉個例子:
在Android系統中會為每個應用(程式)限制最大內容,單個程式超過這個閾值會引起OOM,而使用多程式技術可以規避這個記憶體溢位的問題。再舉個例子:Python在實現Python解析器(CPython)時引入了GIL鎖,使得任何時候僅有
一個執行緒在執行,多執行緒的效率可能還比不上單執行緒,使用多程式技術可以
規避這個限制。

再說說「多執行緒」,首先為什麼會引入執行緒的概念呢?舉個例子:

你有一個文字程式,三個功能組成部分:接收使用者的輸入,顯示到螢幕上,儲存到硬碟裡,如果由三個程式組成:輸入接收程式A,顯示內容程式B,寫入硬碟程式C,而他們之間共同需要擁有的東西——文字內容,而程式A,B,C執行在不同的記憶體空間,這就涉及到程式通訊問題了,而頻繁
的程式切換勢必導致效能上的損失。有沒有一種機制使得做這三個任務時共享資源呢?這個時候執行緒(輕量級的程式)就派上用場了,多個執行緒共享程式資料。相信讀者看到這裡,對於多程式和多執行緒
的概念應該有個初步的瞭解了,接下來簡單比較下兩者的優劣和使用場景:

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

2、執行緒的生命週期

執行緒的生命週期如圖所示

各個狀態說明

  • 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完全可以不依賴於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鎖搶奪的問題,但是也增加程式實現執行緒間資料通訊和同步時的成本,這個需要自行進行權衡。


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相容。

相關文章