您現在的位置是:首頁 > 舞蹈首頁舞蹈

一文講透“程序、執行緒、協程”

由 佳雲大腦 發表于 舞蹈2021-09-05
簡介以檔案寫入為例:程序p1發出資料寫入磁碟檔案的請求CPU處理寫入請求,透過程式設計告訴DMA引擎資料在記憶體的位置,要寫入資料的大小以及目標裝置等資訊CPU處理其他程序p2的請求,DMA負責將記憶體資料寫入到裝置中DMA完成資料傳輸,中斷C

協程和執行緒有什麼差別,優勢呢?

本文從作業系統原理出髮結合程式碼實踐講解了以下內容:

什麼是程序,執行緒和協程?

它們之間的關係是什麼?

為什麼說Python中的多執行緒是偽多執行緒?

不同的應用場景該如何選擇技術方案?

。。。

什麼是程序

程序-作業系統提供的抽象概念,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。程式是指令、資料及其組織形式的描述,程序是程式的實體。程式本身是沒有生命週期的,它只是存在磁碟上的一些指令,程式一旦執行就是程序。

當程式需要執行時,作業系統將程式碼和所有靜態資料記載到記憶體和程序的地址空間(每個程序都擁有唯一的地址空間,見下圖所示)中,透過建立和初始化棧(區域性變數,函式引數和返回地址)、分配堆記憶體以及與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透過地址翻譯將虛擬地址轉換成真實的物理地址,每個程序只能訪問自己的地址空間。因此,在沒有其他機制(程序間通訊)的輔助下,程序之間是無法共享資料的

以python中multiprocessing為例

import multiprocessingimport threadingimport timen = 0def count(num): global n for i in range(100000): n += i print(“Process {0}:n={1},id(n)={2}”。format(num, n, id(n)))if __name__ == ‘__main__’: start_time = time。time() process = list() for i in range(5): p = multiprocessing。Process(target=count, args=(i,)) # 測試多程序使用 # p = threading。Thread(target=count, args=(i,)) # 測試多執行緒使用 process。append(p) for p in process: p。start() for p in process: p。join() print(“Main:n={0},id(n)={1}”。format(n, id(n))) end_time = time。time() print(“Total time:{0}”。format(end_time - start_time))

結果

Process 1:n=4999950000,id(n)=139854202072440Process 0:n=4999950000,id(n)=139854329146064Process 2:n=4999950000,id(n)=139854202072400Process 4:n=4999950000,id(n)=139854201618960Process 3:n=4999950000,id(n)=139854202069320Main:n=0,id(n)=9462720Total time:0。03138256072998047

變數n在程序p{0,1,2,3,4}和主程序(main)中均擁有唯一的地址空間

什麼是執行緒

執行緒-也是作業系統提供的抽象概念,是程式執行中一個單一的順序控制流程,是程式執行流的最小單元,是處理器排程和分派的基本單位。一個程序可以有一個或多個執行緒,同一程序中的多個執行緒將共享該程序中的全部系統資源,如虛擬地址空間,檔案描述符和訊號處理等等。但同一程序中的多個執行緒有各自的呼叫棧和執行緒本地儲存(如下圖所示)。

一文講透“程序、執行緒、協程”

系統利用PCB來完成對程序的控制和管理。同樣,系統為執行緒分配一個執行緒控制塊TCB(Thread Control Block),將所有用於控制和管理執行緒的資訊記錄線上程的控制塊中,TCB中通常包括:

執行緒標誌符

一組暫存器

執行緒執行狀態

優先順序

執行緒專有儲存區

訊號遮蔽

和程序一樣,執行緒同樣有五種狀態:初始態、執行狀態、等待(阻塞)狀態、就緒狀態和終止狀態,執行緒之間的切換和程序一樣也需要上下文切換,這裡不再贅述。

程序和執行緒之間有許多相似的地方,那它們之間到底有什麼區別呢?

程序 VS 執行緒

程序是資源的分配和排程的獨立單元。程序擁有完整的虛擬地址空間,當發生程序切換時,不同的程序擁有不同的虛擬地址空間。而同一程序的多個執行緒是可以共享同一地址空間

執行緒是CPU排程的基本單元,一個程序包含若干執行緒。

執行緒比程序小,基本上不擁有系統資源。執行緒的建立和銷燬所需要的時間比程序小很多

由於執行緒之間能夠共享地址空間,因此,需要考慮同步和互斥操作

一個執行緒的意外終止會影像整個程序的正常執行,但是一個程序的意外終止不會影像其他的程序的執行。因此,多程序程式安全性更高。

總之,多程序程式安全性高,程序切換開銷大,效率低;多執行緒程式維護成本高,執行緒切換開銷小,效率高。(

python的多執行緒是偽多執行緒,下文中將詳細介紹

什麼是協程

協程(Coroutine,又稱微執行緒)是一種比執行緒更加輕量級的存在,協程不是被作業系統核心所管理,而完全是由程式所控制。協程與執行緒以及程序的關係見下圖所示。

協程可以比作子程式,但執行過程中,子程式內部可中斷,然後轉而執行別的子程式,在適當的時候再返回來接著執行。協程之間的切換不需要涉及任何系統呼叫或任何阻塞呼叫

協程只在一個執行緒中執行,是子程式之間的切換,發生在使用者態上。而且,執行緒的阻塞狀態是由作業系統核心來完成,發生在核心態上,因此協程相比執行緒節省執行緒建立和切換的開銷

協程中不存在同時寫變數衝突,因此,也就不需要用來守衛關鍵區塊的同步性原語,比如互斥鎖、訊號量等,並且不需要來自作業系統的支援。

協程適用於IO阻塞且需要大量併發的場景,當發生IO阻塞,由協程的排程器進行排程,透過將資料流yield掉,並且記錄當前棧上的資料,阻塞完後立刻再透過執行緒恢復棧,並把阻塞的結果放到這個執行緒上去執行。

一文講透“程序、執行緒、協程”

下面,將針對在不同的應用場景中如何選擇使用Python中的程序,執行緒,協程進行分析。

如何選擇?

在針對不同的場景對比三者的區別之前,首先需要介紹一下python的多執行緒(一直被程式設計師所詬病,認為是“假的”多執行緒)。

那為什麼認為Python中的多執行緒是“偽”多執行緒呢?

更換上面multiprocessing示例中, p=multiprocessing。Process(target=count,args=(i,))為 p=threading。Thread(target=count,args=(i,)),其他照舊,執行結果如下:

為了減少程式碼冗餘和文章篇幅,命名和列印不規則問題請忽略

Process 0:n=5756690257,id(n)=140103573185600Process 2:n=10819616173,id(n)=140103573185600Process 1:n=11829507727,id(n)=140103573185600Process 4:n=17812587459,id(n)=140103573072912Process 3:n=14424763612,id(n)=140103573185600Main:n=17812587459,id(n)=140103573072912Total time:0。1056210994720459

n是全域性變數,Main的列印結果與執行緒相等,證明了執行緒之間是資料共享

但是,為什麼多執行緒執行時間比多程序還要長?這與我們上面所說(執行緒的開銷<<程序的開銷)的嚴重不相符啊。這就是輪到Cpython(python預設的直譯器)中GIL(Global Interpreter Lock,全域性解釋鎖)登場了。

什麼是GIL

GIL來源於Python設計之初的考慮,為了資料安全(由於記憶體管理機制中採用引用計數)所做的決定。某個執行緒想要執行,必須先拿到 GIL。因此,可以把 GIL 看作是“通行證”,並且在一個 Python程序中,GIL 只有一個,拿不到通行證的執行緒,就不允許進入 CPU 執行。

Cpython直譯器在記憶體管理中採用引用計數,當物件的引用次數為0時,會將物件當作垃圾進行回收。設想這樣一種場景:

一個程序中含有兩個執行緒,分別為執行緒0和執行緒1,兩個執行緒全都引用物件a。當兩個執行緒同時對a發生引用(並未修改,不需要使用同步性原語),就會發生同時修改物件a的引用計數器,造成計數器引用少於實質性的引用,當進行垃圾回收時,造成錯誤異常。因此,需要一把全域性鎖(即為GIL)來保證物件引用計數的正確性和安全性。

無論是單核還是多核,一個程序永遠只能同時執行一個執行緒(拿到 GIL 的執行緒才能執行,如下圖所示),這就是為什麼在多核CPU上,Python 的多執行緒效率並不高的根本原因。

一文講透“程序、執行緒、協程”

那是不是在Python中遇到併發的需求就使用多程序就萬事大吉了呢?其實不然,軟體工程中有一句名言:

沒有銀彈!

何時用?

常見的應用場景不外乎三種:

CPU密集型:程式需要佔用CPU進行大量的運算和資料處理;

I/O密集型:程式中需要頻繁的進行I/O操作;例如網路中socket資料傳輸和讀取等;

CPU密集+I/O密集:以上兩種的結合

CPU密集型的情況可以對比以上multiprocessing和threading的例子,多程序的效能 > 多執行緒的效能。

下面主要解釋一下I/O密集型的情況。與I/O裝置互動,目前最常用的解決方案就是

DMA

什麼是DMA

DMA(Direct Memory Access)是系統中的一個特殊裝置,它可以協調完成記憶體到裝置間的資料傳輸,中間過程不需要CPU介入。

以檔案寫入為例:

程序p1發出資料寫入磁碟檔案的請求

CPU處理寫入請求,透過程式設計告訴DMA引擎資料在記憶體的位置,要寫入資料的大小以及目標裝置等資訊

CPU處理其他程序p2的請求,DMA負責將記憶體資料寫入到裝置中

DMA完成資料傳輸,中斷CPU

CPU從p2上下文切換到p1,繼續執行p1

一文講透“程序、執行緒、協程”

Python多執行緒的表現(I/O密集型)

執行緒Thread0首先執行,執行緒Thread1等待(GIL的存在)

Thread0收到I/O請求,將請求轉發給DMA,DMA執行請求

Thread1佔用CPU資源,繼續執行

CPU收到DMA的中斷請求,切換到Thread0繼續執行

一文講透“程序、執行緒、協程”

與程序的執行模式相似,彌補了GIL帶來的不足,又由於執行緒的開銷遠遠小於程序的開銷,因此,在IO密集型場景中,多執行緒的效能更高

實踐是檢驗真理的唯一標準,下面將針對I/O密集型場景進行測試。

測試

執行程式碼

import multiprocessingimport threadingimport timedef count(num): time。sleep(1) ## 模擬IO操作 print(“Process {0} End”。format(num))if __name__ == ‘__main__’: start_time = time。time() process = list() for i in range(5): p = multiprocessing。Process(target=count, args=(i,)) # p = threading。Thread(target=count, args=(i,)) process。append(p) for p in process: p。start() for p in process: p。join() end_time = time。time() print(“Total time:{0}”。format(end_time - start_time))

結果

## 多程序Process 0 EndProcess 3 EndProcess 4 EndProcess 2 EndProcess 1 EndTotal time:1。383193016052246## 多執行緒Process 0 EndProcess 4 EndProcess 3 EndProcess 1 EndProcess 2 EndTotal time:1。003425121307373

多執行緒的執行效效能高於多程序

是不是認為這就結束了?遠還沒有呢。針對I/O密集型的程式,協程的執行效率更高,因為它是程式自身所控制的,這樣將節省執行緒建立和切換所帶來的開銷。

以Python中asyncio應用為依賴,使用async/await語法進行協程的建立和使用。

程式程式碼

import timeimport asyncioasync def coroutine(): await asyncio。sleep(1) ## 模擬IO操作if __name__ == “__main__”: start_time = time。time() loop = asyncio。get_event_loop() tasks = [] for i in range(5): task = loop。create_task(coroutine()) tasks。append(task) loop。run_until_complete(asyncio。wait(tasks)) loop。close() end_time = time。time() print(“total time:”, end_time - start_time)

結果

total time: 1。001854419708252

協程的執行效效能高於多執行緒

總結

本文從作業系統原理出髮結合程式碼實踐講解了程序,執行緒和協程以及他們之間的關係。並且,總結和整理了Python實踐中針對不同的場景如何選擇對應的方案,如下:

CPU密集型:多程序

IO密集型:多執行緒(協程維護成本較高,而且在讀寫檔案方面效率沒有顯著提升)

CPU密集和IO密集:多程序+協程

—————END—————