tft每日頭條

 > 圖文

 > 什麼是進程線程協程

什麼是進程線程協程

圖文 更新时间:2024-08-09 13:26:53

本文從操作系統原理出發結合代碼實踐講解了以下内容:

什麼是進程,線程和協程?

它們之間的關系是什麼?

為什麼說Python中的多線程是僞多線程?

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

...

什麼是進程

進程-操作系統提供的抽象概念,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎。程序是指令、數據及其組織形式的描述,進程是程序的實體。程序本身是沒有生命周期的,它隻是存在磁盤上的一些指令,程序一旦運行就是進程。

當程序需要運行時,操作系統将代碼和所有靜态數據記載到内存和進程的地址空間(每個進程都擁有唯一的地址空間,見下圖所示)中,通過創建和初始化棧(局部變量,函數參數和返回地址)、分配堆内存以及與IO相關的任務,當前期準備工作完成,啟動程序,OS将CPU的控制權轉移到新創建的進程,進程開始運行。

什麼是進程線程協程(一文講透進程線程)1

操作系統對進程的控制和管理通過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)中均擁有唯一的地址空間

什麼是線程

線程-也是操作系統提供的抽象概念,是程序執行中一個單一的順序控制流程,是程序執行流的最小單元,是處理器調度和分派的基本單位。一個進程可以有一個或多個線程,同一進程中的多個線程将共享該進程中的全部系統資源,如虛拟地址空間,文件描述符和信号處理等等。但同一進程中的多個線程有各自的調用棧和線程本地存儲(如下圖所示)。

什麼是進程線程協程(一文講透進程線程)2

系統利用PCB來完成對進程的控制和管理。同樣,系統為線程分配一個線程控制塊TCB(Thread Control Block),将所有用于控制和管理線程的信息記錄在線程的控制塊中,TCB中通常包括:

  • 線程标志符
  • 一組寄存器
  • 線程運行狀态
  • 優先級
  • 線程專有存儲區
  • 信号屏蔽

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

進程和線程之間有許多相似的地方,那它們之間到底有什麼區别呢?

進程 VS 線程
  • 進程是資源的分配和調度的獨立單元。進程擁有完整的虛拟地址空間,當發生進程切換時,不同的進程擁有不同的虛拟地址空間。而同一進程的多個線程是可以共享同一地址空間
  • 線程是CPU調度的基本單元,一個進程包含若幹線程。
  • 線程比進程小,基本上不擁有系統資源。線程的創建和銷毀所需要的時間比進程小很多
  • 由于線程之間能夠共享地址空間,因此,需要考慮同步和互斥操作
  • 一個線程的意外終止會影像整個進程的正常運行,但是一個進程的意外終止不會影像其他的進程的運行。因此,多進程程序安全性更高。

總之,多進程程序安全性高,進程切換開銷大,效率低;多線程程序維護成本高,線程切換開銷小,效率高。(python的多線程是僞多線程,下文中将詳細介紹

什麼是協程

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

  • 協程可以比作子程序,但執行過程中,子程序内部可中斷,然後轉而執行别的子程序,在适當的時候再返回來接着執行。協程之間的切換不需要涉及任何系統調用或任何阻塞調用
  • 協程隻在一個線程中執行,是子程序之間的切換,發生在用戶态上。而且,線程的阻塞狀态是由操作系統内核來完成,發生在内核态上,因此協程相比線程節省線程創建和切換的開銷
  • 協程中不存在同時寫變量沖突,因此,也就不需要用來守衛關鍵區塊的同步性原語,比如互斥鎖、信号量等,并且不需要來自操作系統的支持。

協程适用于IO阻塞且需要大量并發的場景,當發生IO阻塞,由協程的調度器進行調度,通過将數據流yield掉,并且記錄當前棧上的數據,阻塞完後立刻再通過線程恢複棧,并把阻塞的結果放到這個線程上去運行。

什麼是進程線程協程(一文講透進程線程)3

下面,将針對在不同的應用場景中如何選擇使用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 的多線程效率并不高的根本原因。

什麼是進程線程協程(一文講透進程線程)4

那是不是在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

什麼是進程線程協程(一文講透進程線程)5

Python多線程的表現(I/O密集型)
  • 線程Thread0首先執行,線程Thread1等待(GIL的存在)
  • Thread0收到I/O請求,将請求轉發給DMA,DMA執行請求
  • Thread1占用CPU資源,繼續執行
  • CPU收到DMA的中斷請求,切換到Thread0繼續執行

什麼是進程線程協程(一文講透進程線程)6

與進程的執行模式相似,彌補了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—————

,

更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!

查看全部

相关圖文资讯推荐

热门圖文资讯推荐

网友关注

Copyright 2023-2024 - www.tftnews.com All Rights Reserved