分類目錄:《系統學習Python》總目錄
并行是并發的一種特殊情況。**所有并行系統都是并發的,但不是所有并發系統都是并行的。**在21世紀初,我們可以使用單核設備在GNU Linux上同時處理100個進程。一臺擁有4個CPU核的現代筆記本計算機,在正常情況下,任何時間段內運行的進程數隨隨便便都會超過200個。如果并行執行200個任務,則需要200個核。因此,多數計算實際是并發的,而不是并行的。操作系統管理著數百個進程,確保每個進程都有機會取得進展,即使CPU本身同時做的事情不能超過4件。
《系統學習Python——并發模型和異步編程》系列文章假定我們事先不具備并發或并行編程知識。我們會簡要介紹相關概念之后,將通過簡單的示例學習和比較Python為并發編程提供的3個核心包:threading
、multiprocessing
和asyncio
。我們還會講解增強Python應用性能和伸縮性的第三方工具、庫、應用服務器和分布式任務隊列。同時,我們也會講解Python的3種并發方式:線程、進程和原生協程。
導致并發編程困難的因素很多,但我們會講到啟動線程或進程以及如何跟蹤線程或進程。調用一個函數,發出調用的代碼開始阻塞,直到函數返回。因此,我們知道函數什么時候執行完畢,而且能輕松地得到函數的返回值。如果函數可能拋出異常,則把函數調用放在try
/except
塊中,捕獲錯誤。這些熟悉的概念在我們啟動線程或進程后都不可用了。同時,我們無法輕松地得知操作何時結束,若想獲取結果或捕獲錯誤,則需要設置某種通信信道,例如消息隊列。此外,啟動線程或進程有一定消耗,僅僅為了計算一個結果就退出,肯定得不償失。通常,更好的選擇是讓各個線程或進程進入一個職程(Worker),循環等待要處理的輸入,以此分攤啟動成本。但是,這又進一步增加了通信難度,還會引起更多問題。如果不需要職程了,那么如何退出呢?怎樣退出才能做到不中斷作業,避免留下未處理完畢的數據和未釋放的資源(例如打開的文件)呢?同樣,解決這些問題通常涉及消息和隊列。協程的啟動成本很低。使用await
關鍵字啟動的協程,返回值容易獲取,可以安全取消,捕獲異常的位置也明確。但是,協程通常由異步框架啟動,因此監控難度與線程或進程相當。
最后,我們還會說明Python協程和線程不適合CPU密集型任務。鑒于此,并發編程需要學習新的概念和編程模式。首先,我們要對核心概念確立統一認識。
術語定義
- 并發:處理多個待定任務,一次處理一個或并行處理多個(如果條件允許)?,直到所有任務最終都成功或失敗。對于單核CPU,如果操作系統的調度程序支持交叉執行待定任務,也能實現并發。并發也叫多任務處理(Multitasking)。
- 并行:同時執行多個計算任務的能力。需要一個多核CPU、多個CPU、一個GPU或一個集群中的多臺計算機。
- 執行單元:并發執行代碼的對象的統稱,每個對象的狀態和調用棧是獨立的。Python原生支持3種執行單元:進程、線程和協程。
- 進程:計算機程序運行時的一個實例,消耗內存和部分CPU時間。現代桌面操作系統通常同時管理數百個進程,每個進程都隔離在自己的私有內存空間中。進程通過管道、套接字或內存映射文件進行通信—這些方式都只能攜帶原始字節。Python對象必須序列化(轉換)為原始字節才能從一個進程傳遞到另一個進程。這個過程耗費資源,而且不是所有Python對象都可以序列化。**進程可以派生子進程,子進程彼此之間以及與父進程之間是隔離的。**進程支持搶占式多任務處理機制:操作系統調度程序定期搶占(掛起)運行中的進程,讓其他進程運行。這意味著凍結的進程理論上不會凍結整個系統。
- 線程:單個進程中的執行單元。**一個進程啟動后,只使用一個線程,即主線程。通過調用操作系統API,進程可以創建更多線程,執行并發操作。**一個進程內的線程共享相同的內存空間(存儲活動的Python對象)?。因此,線程之間可以輕松地共享數據,但是如果多個線程同時更新同一個對象,則可能導致數據損壞。與進程一樣,線程在操作系統調度程序的監督下也可以實現搶占式多任務處理。對于同一份作業,線程消耗的資源比進程少。
- 協程:可以掛起自身并在以后恢復的函數。在Python中,經典協程由生成器函數構建,原生協程使用
async def
定義。我們在已經《系統學習Python》之前的文章中介紹過經典協程,原生協程的用法將在《系統學習Python——并發模型和異步編程》系列文章中討論。Python協程通常在事件循環(也在同一個線程中)的監督下在單個線程中運行。asyncio
、Curio
或Trio
等異步編程框架為基于協程的非阻塞I/O提供了事件循環和支持庫。協程支持協作式多任務處理:一個協程必須使用yield
或await
關鍵字顯式放棄控制權,另一個協程才可以并發(而非并行)開展工作。這意味著,協程中只要有導致阻塞的代碼,事件循環和其他所有協程的執行就都會受到阻塞,這一點與進程和線程的搶占式多任務處理形成鮮明對比。另外,對于同一份作業,協程消耗的資源比線程或進程少。 - 隊列:一種數據結構,可以放入和取出項,順序通常是先入先出(FIFO)。獨立的執行單元可以通過隊列交換應用數據和控制消息,例如錯誤代碼和終止信號。隊列的實現因底層并發模型而異:Python標準庫中的
queue
包提供的隊列類支持線程,multiprocessing
和asyncio
包則實現了其他隊列類。queue
和asyncio
包中還有非先入先出隊列:LifoQueue
和PriorityQueue
。 - 鎖:一種供執行單元用來同步操作和避免數據損壞的對象。更新共享數據結構時,當前代碼應持有相關的鎖,并告訴程序的其他部分等到鎖被釋放后再訪問這個數據結構。最簡單的鎖是互斥鎖。鎖的實現取決于底層并發模型。
- 爭用:對有限資源的爭奪。當多個執行單元嘗試訪問共享資源(例如鎖或存儲器)時,就會發生資源爭用。當計算密集型進程或線程必須等待操作系統調度程序為其分配CPU時間時,還會發生CPU爭用。
參考文獻:
[1] Mark Lutz. Python學習手冊[M]. 機械工業出版社, 2018.
[2] 盧西亞諾·拉馬略.流暢的Python 第2版(全2冊) 編程語言[M].人民郵電出版社,2023.