一文講透 「進程、線程、協程」

2020-12-25 酷扯兒

以下文章來源於程式設計師小灰 ,作者頭文件

程式設計師小灰

一群喜愛編程技術和算法的小倉鼠。

本文從作業系統原理出髮結合代碼實踐講解了以下內容:什麼是進程,線程和協程?它們之間的關係是什麼?為什麼說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 multiprocessing

import threading

import time

n = 0

def 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)=139854202072440

Process 0:n=4999950000,id(n)=139854329146064

Process 2:n=4999950000,id(n)=139854202072400

Process 4:n=4999950000,id(n)=139854201618960

Process 3:n=4999950000,id(n)=139854202069320

Main:n=0,id(n)=9462720

Total 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)=140103573185600

Process 2:n=10819616173,id(n)=140103573185600

Process 1:n=11829507727,id(n)=140103573185600

Process 4:n=17812587459,id(n)=140103573072912

Process 3:n=14424763612,id(n)=140103573185600

Main:n=17812587459,id(n)=140103573072912

Total 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完成數據傳輸,中斷CPUCPU從p2上下文切換到p1,繼續執行p1

Python多線程的表現(I/O密集型)

線程Thread0首先執行,線程Thread1等待(GIL的存在)Thread0收到I/O請求,將請求轉發給DMA,DMA執行請求Thread1佔用CPU資源,繼續執行CPU收到DMA的中斷請求,切換到Thread0繼續執行

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

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

測試

執行代碼

import multiprocessing

import threading

import time

def 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 End

Process 3 End

Process 4 End

Process 2 End

Process 1 End

Total time:1.383193016052246

## 多線程

Process 0 End

Process 4 End

Process 3 End

Process 1 End

Process 2 End

Total time:1.003425121307373

多線程的執行效性能高於多進程

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

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

程序代碼

import time

import asyncio

async 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密集:多進程+協程

相關焦點

  • 協程是什麼?為什麼越來越多的程式設計師開始使用協程?
    本文基於python01協程相關的概念想要了解協程,必須先簡單說下進程和線程。進程和線程都是作業系統之下的概念,而協程則是程式設計師自己設計的代碼運行過程。進程在作業系統中,每一個獨立運行的程序,都佔有作業系統分配的資源,這些程序互不幹涉,都只負責運行自己的指令,這就是進程線程線程是進程中的一個實體,是被系統獨立調度和分派的基本單位
  • 最牛的異步程序寫法——協程
    要講清楚協程的原理非常難,我也只是了解一點皮毛,不過我還是很想把這些皮毛跟大家分享一下,首先跟大家說一下多進程、多線程和協程的原理。進程是系統分配資源的最小單位,多進程耗費系統資源,一般不會使用,而一個進程又可以包含多個線程,多線程要比多進程節省資源,在Python中由於全局解釋器鎖的存在,一個進程中同一時間最多只能執行一個線程,所以Python原生是不支持異步並發的。
  • 知乎轉載:也來談談協程
    但這樣就出現了很多新問題:如果下載電影的進程速度更慢,視頻播放器讀到尚未填充有效數據的區域怎麼辦?或者,電影下載進程下了100k數據,一校驗,是壞的。結果視頻播放器快手快腳拿過去就放;剛放了一幀,電影下載進程作廢了這段數據,把讀指針跳回到100k前;而電影播放器呢,它還保留著一個無效的讀指針……這時候,程式設計師就不得不做很多的同步工作。
  • 每日一課丨Python爬蟲:單線程、多線程和協程的爬蟲性能對比
    、多線程和協程來爬取,從而對比單線程、多線程和協程在網絡爬蟲中的性能。多線程爬蟲單線程的爬取耗時還是挺長的,下面看看使用多線程的爬取效率:import requestsfrom lxml import etreeimport pandas
  • 一文讀懂Linux進程、進程組、會話、殭屍
    GitHub:     https://github.com/rongweihe個人博客:   https://rongweihe.github.io/1、前言在研究 Linux 實現之前,首先要對進程、進程組、會話,線程有個整體的了解:一個會話包含多個進程組,一個進程組包含多個進程,一個進程包含多個線程。
  • Swoole v4.6.0 版本發布,支持原生 curl 協程客戶端
    將 Event::rshutdown() 標記為已棄用,請改用 Coroutine\run在之前的版本中,如果在index.php中直接使用go創建協程go(function () {    var_dump(Co\System::gethostbyname('www.baidu.com
  • 吹爆系列:探索 Android 多線程的一切
    吹爆系列:深入實戰Android卡頓優化吹爆系列:深度探索 Gradle 自動化構建技術吹爆系列:深入探索Android卡頓優化吹爆系列:深入探索Android布局優化大科普吹爆系列:深入探索 Android 包體積優化吹爆系列:深入探索Android穩定性優化
  • python GIL與多線程是什麼關係呢?
    問題一:列表 self append 無限嵌套的原理先來回答第一個問題,兩個同學都問到了,下面這段代碼中的 x,為什麼是無限嵌套的列表?前面我們講過,GIL 是指同一時刻,程序只能有一個線程運行;而 Python 中的多線程,是指多個線程交替執行,造成一個「偽並行」的結果,但是具體到某一時刻,仍然只有 1 個線程在運行,並不是真正的多線程並行。這個機制,我畫了下面這張圖來表示:舉個例子來理解。比如,我用 10 個線程來爬取 50 個網站的內容。
  • 鴻蒙內核源碼分析:Task/線程管理篇
    線程可以使用或等待CPU、使用內存空間等系統資源,並獨立於其它線程運行。鴻蒙內核每個進程內的線程獨立運行、獨立調度,當前進程內線程的調度不受其它進程內線程的影響。鴻蒙內核中的線程採用搶佔式調度機制,同時支持時間片輪轉調度和FIFO調度方式。鴻蒙內核的線程一共有32個優先級(0-31),最高優先級為0,最低優先級為31。
  • Linux 進程管理數據結構
    文末集贊留言抽獎,我會選出留言點讚數前 3 名送出小米耳機。別刷贊啊,刷贊被舉報無效,相信真的是公眾號粉絲的讀者,不會做這樣的行為,刷贊指的是購買外掛刷,如果是轉發到朋友圈和微信群的,不算刷贊行為。數據結構Linux 內核使用 task_struct 來表示一個進程,這個結構體裡面保存了進程的所有信息,要研究進程的數據結構,我就就需要研究這個結構體裡面各個成員的作用。
  • Linux 殭屍進程可以被殺死嗎?
    殭屍進程不可能被殺死,因為它已經死了,不存在再死一次的問題。死的對立面是活,死者已死。只有活的進程才可能被殺死。什麼是殭屍進程?首先要明確一點,殭屍進程的含義是:子進程已經死了,但是父進程還沒有wait它的一個中間狀態,這個時候子進程是一個殭屍。
  • 由於ZombieLoad漏洞,英特爾的CPU超線程可能要涼了
    這次ZombieLoad同樣讓英特爾十分的絕望,新推出的這些軟體補丁根本沒有辦法消除這個漏洞所帶來的不完全因素,如果想要絕對的安全就要完全關閉超線程。雖然AMD銳龍同樣有著類似的超線程功能,不過並不受這次ZombieLoad漏洞的影響,因此英特爾這一回需要獨自面對這次的挑戰,這個漏洞的主要危害是可能會讓惡意的進程進入到緩衝區然後盜取正常進程裡的機密數據。
  • Java中的多線程你只要看這一篇就夠了
    作者:納達丶無忌          來源:簡書https://www.jianshu.com/p/40d4c7aebd66源碼共讀整理髮布,轉載請聯繫作者獲得授權引如果對什麼是線程、什麼是進程仍存有疑惑
  • Lisa Su:下一代線程撕裂者當然有!
    銳龍3000處理器發布之後,大家對於線程撕裂者有了更多期待。不過時至今日,AMD方面並沒有關於線程撕裂者的消息,這讓很多朋友感到疑惑。就在大家還不確定是否會有下一代線程撕裂者的時候,AMD CEO Lisa Su給出了明確答覆。
  • 一文講透
    一文帶你學會如何用Python生成帶誤差棒的並列和堆積柱狀圖一文講透,帶你學會用Python繪製帶誤差棒的柱狀圖和條形圖一文幫你用Python繪製,拿走不謝敬請關注「品位集結號」,使您每天進步一點點!
  • 我用多線程爬了5000多部最新電影下載連結
    一看這種網站,應該不會有什麼反爬措施,按照正常套路,先把跳轉進去的連結全部弄出來。但是這裡還是有很多的小細節,例如我們需要拿到電影的總頁數,其次這麼多的頁面,一個線程不知道要跑到什麼時候,所以我們首先先拿到總頁碼,然後用多線程來進行任務的分配。
  • 一文講透,把話說得「一針見血」
    一文講透,把話說得「一針見血」;假如你說話總是說不到點子上,建議看看這篇文章,提高口才和情商。在工作及日常交往中,常會見到這樣一些人:他們有的口若懸河,有的則木訥少語,但給人留下的印象總是話說得不到位——「說不到點子上」。客觀來說,限於時境及其他因素,每個人都曾有過話「說不到點子上」的時候,關鍵要看是否經常如此。