what's the 進程
進程是操作系統提供的抽象概念,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。
程序是指令、數據及其組織形式的描述,進程是程序的實體。程序本身是沒有生命周期的,它只是存在磁盤上的一些指令,程序一旦運行就是進程。
當程序需要運行時,操作系統將代碼和所有靜態數據記載到內存和進程的地址空間(每個進程都擁有唯一的地址空間,見下圖所示)中,通過創建和初始化棧(局部變量,函數參數和返回地址)、分配堆內存以及與IO相關的任務,當前期准備工作完成,啟動程序,OS將CPU的控制權轉移到新創建的進程,進程開始運行。
操作系統對進程的控制和管理通過 PCB(Processing Control Block),PCB 通常是系統內存占用區中的一個連續存區,它存放着操作系統用於描述進程情況及控制進程運行所需的全部信息(進程標識號,進程狀態,進程優先級,文件系統指針以及各個寄存器的內容等),進程的 PCB 是系統感知進程的唯一實體。
進程的狀態
一個進程至少具有5種基本狀態:初始態、執行狀態、等待(阻塞)狀態、就緒狀態、終止狀態
-
初始狀態:進程剛被創建,由於其他進程正占有 CPU 所以得不到執行,只能處於初始狀態。
-
執行狀態:任意時刻處於執行狀態的進程只能有一個。
-
就緒狀態:只有處於就緒狀態的經過調度才能到執行狀態
-
等待狀態:進程等待某件事件完成
-
停止狀態:進程結束
進程間的切換
無論是在多核還是單核系統中,一個 CPU 看上去都像是在並發的執行多個進程,這是通過處理器在進程間切換來實現的。
操作系統對把 CPU 控制權在不同進程之間交換執行的機制稱為上下文切換(context switch),即保存當前進程的上下文,恢復新進程的上下文,然后將 CPU 控制權轉移到新進程,新進程就會從上次停止的地方開始。因此,進程是輪流使用 CPU 的,CPU 被若干進程共享,使用某種調度算法來決定何時停止一個進程,並轉而為另一個進程提供服務。
- 單核CPU雙進程的情況:進程直接特定的機制和遇到I/O中斷的情況下,進行上下文切換,輪流使用CPU資源
- 雙核CPU雙進程的情況:每一個進程獨占一個CPU核心資源,在處理I/O請求的時候,CPU處於阻塞狀態
進程間數據共享
系統中的進程與其他進程共享 CPU 和主存資源,為了更好的管理主存,現在系統提供了一種對主存的抽象概念,即為虛擬存儲器(VM)。它是一個抽象的概念,它為每一個進程提供了一個假象,即每個進程都在獨占地使用主存。
虛擬存儲器主要提供了三個能力:
-
將主存看成是一個存儲在磁盤上的高速緩存,在主存中只保存活動區域,並根據需要在磁盤和主存之間來回傳送數據,通過這種方式,更高效地使用主存
-
為每個進程提供了一致的地址空間,從而簡化了存儲器管理
-
保護了每個進程的地址空間不被其他進程破壞
由於進程擁有自己獨占的虛擬地址空間,CPU 通過地址翻譯將虛擬地址轉換成真實的物理地址,每個進程只能訪問自己的地址空間。因此,在沒有其他機制(進程間通信)的輔助下,進程之間是無法共享數據的。關於進程間通信的方式,可以看另一篇博文的內容——
what's the 線程
線程也是操作系統提供的抽象概念,是程序執行中一個單一的順序控制流程,是程序執行流的最小單元,是處理器調度和分派的基本單位。
一個進程可以有一個或多個線程,同一進程中的多個線程將共享該進程中的全部系統資源,如虛擬地址空間,文件描述符和信號處理等等。但同一進程中的多個線程有各自的調用棧和線程本地存儲(如下圖所示)。
系統利用 PCB 來完成對進程的控制和管理。同樣,系統為線程分配一個線程控制塊 TCB(Thread Control Block),將所有用於控制和管理線程的信息記錄在線程的控制塊中
TCB 中通常包括:
-
線程標志符
-
一組寄存器
-
線程運行狀態
-
優先級
-
線程專有存儲區
-
信號屏蔽
線程的狀態
和進程一樣,線程同樣有五種狀態:初始態、執行狀態、等待(阻塞)狀態、就緒狀態和終止狀態,線程之間的切換和進程一樣也需要上下文切換,這里不再贅述。
進程 VS 線程
-
進程是資源的分配和調度的獨立單元。進程擁有完整的虛擬地址空間,當發生進程切換時,不同的進程擁有不同的虛擬地址空間。而同一進程的多個線程是可以共享同一地址空間
-
線程是 CPU 調度的基本單元,一個進程包含若干線程。
-
線程比進程小,基本上不擁有系統資源。線程的創建和銷毀所需要的時間比進程小很多
-
由於線程之間能夠共享地址空間,因此,需要考慮同步和互斥操作
-
一個線程的意外終止會影響整個進程的正常運行,但是一個進程的意外終止不會影響其他的進程的運行。因此,多進程程序安全性更高。
what's the 協程
協程(Coroutine,又稱微線程)是一種比線程更加輕量級的存在,協程不是被操作系統內核所管理,而完全是由程序所控制。協程與線程以及進程的關系見下圖所示。
-
協程可以比作子程序,但執行過程中,子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接着執行。協程之間的切換不需要涉及任何系統調用或任何阻塞調用
-
協程只在一個線程中執行,是子程序之間的切換,發生在用戶態上。而且,線程的阻塞狀態是由操作系統內核來完成,發生在內核態上,因此協程相比線程節省線程創建和切換的開銷
-
協程中不存在同時寫變量沖突,因此,也就不需要用來守衛關鍵區塊的同步性原語,比如互斥鎖、信號量等,並且不需要來自操作系統的支持。
協程適用於 IO 阻塞且需要大量並發的場景,當發生 IO 阻塞,由協程的調度器進行調度,通過將數據流 yield 掉,並且記錄當前棧上的數據,阻塞完后立刻再通過線程恢復棧,並把阻塞的結果放到這個線程上去運行。
方案選用
在針對不同的場景對比三者的區別之前,首先需要介紹一下 python 的多線程(一直被程序員所詬病,認為是"假的"多線程)。
GIL
GIL 來源於 Python 設計之初的考慮,為了數據安全(由於內存管理機制中采用引用計數)所做的決定。某個線程想要執行,必須先拿到 GIL。因此,可以把 GIL 看作是“通行證”,並且在一個 Python 進程中,GIL 只有一個,拿不到通行證的線程,就不允許進入 CPU 執行。
Cpython 解釋器在內存管理中采用引用計數,當對象的引用次數為 0 時,會將對象當作垃圾進行回收。設想這樣一種場景:一個進程中含有兩個線程,分別為線程 0 和線程 1,兩個線程全都引用對象 a。當兩個線程同時對 a 發生引用(並未修改,不需要使用同步性原語),就會發生同時修改對象 a 的引用計數器,造成計數器引用少於實質性的引用,當進行垃圾回收時,造成錯誤異常。因此,需要一把全局鎖(即為 GIL)來保證對象引用計數的正確性和安全性。
無論是單核還是多核,一個進程永遠只能同時執行一個線程(拿到 GIL 的線程才能執行,如下圖所示),這就是為什么在多核 CPU 上,Python 的多線程效率並不高的根本原因。
DMA
DMA(Direct Memory Access)是系統中的一個特殊設備,它可以協調完成內存到設備間的數據傳輸,中間過程不需要 CPU 介入。
以文件寫入為例:
-
進程 p1 發出數據寫入磁盤文件的請求
-
CPU 處理寫入請求,通過編程告訴 DMA 引擎數據在內存的位置,要寫入數據的大小以及目標設備等信息
-
CPU 處理其他進程 p2 的請求,DMA 負責將內存數據寫入到設備中
-
DMA 完成數據傳輸,中斷 CPU
-
CPU 從 p2 上下文切換到 p1,繼續執行 p1
方案選用場景
常見的應用場景不外乎三種:
-
CPU密集型:程序需要占用CPU進行大量的運算和數據處理;
-
I/O密集型:程序中需要頻繁的進行I/O操作;例如網絡中socket數據傳輸和讀取等;
-
CPU密集+I/O密集:以上兩種的結合
Python多線程的表現(I/O密集型)
-
線程Thread0首先執行,線程Thread1等待(GIL的存在)
-
Thread0收到I/O請求,將請求轉發給DMA,DMA執行請求
-
Thread1占用CPU資源,繼續執行
-
CPU收到DMA的中斷請求,切換到Thread0繼續執行
與進程的執行模式相似,彌補了GIL帶來的不足,又由於線程的開銷遠遠小於進程的開銷,因此,在 IO 密集型場景中,多線程的性能更高
總結
-
CPU密集型:多進程
-
IO密集型:多線程(協程維護成本較高,而且在讀寫文件方面效率沒有顯著提升)
-
CPU密集和IO密集:多進程+協程
參考:https://mp.weixin.qq.com/s/Yj7O3mucbFHxx3p5UnlvnA