一文講透「進程,線程和協程」

一文講透「進程,線程和協程」 本文從操做系統原理出髮結合代碼實踐講解了如下內容:python

什麼是進程,線程和協程? 它們之間的關係是什麼? 爲何說Python中的多線程是僞多線程? 不一樣的應用場景該如何選擇技術方案? ...程序員

什麼是進程

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

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

1

操做系統對進程的控制和管理經過PCB(Processing Control Block),PCB一般是系統內存佔用區中的一個連續存區,它存放着操做系統用於描述進程狀況及控制進程運行所需的所有信息(包括:進程標識號,進程狀態,進程優先級,文件系統指針以及各個寄存器的內容等),進程的PCB是系統感知進程的惟一實體。編程

一個進程至少具備5種基本狀態:初始態、就緒狀態、等待(阻塞)狀態、執行狀態、終止狀態。緩存

初始狀態:進程剛被建立,因爲其餘進程正佔有CPU資源,因此得不到執行,只能處於初始狀態。 就緒狀態:只有處於就緒狀態的通過調度才能到執行狀態 等待狀態:進程等待某件事件完成 執行狀態:任意時刻處於執行狀態的進程只能有一個(對於單核CPU來說)。 中止狀態:進程結束安全

進程間的切換

不管是在多核仍是單核系統中,一個CPU看上去都像是在併發的執行多個進程,這是經過處理器在進程間切換來實現的。 操做系統對把CPU控制權在不一樣進程之間交換執行的機制稱爲上下文切換(context switch),即保存當前進程的上下文,恢復新進程的上下文,而後將CPU控制權轉移到新進程,新進程就會從上次中止的地方開始。所以,進程是輪流使用CPU的,CPU被若干進程共享,使用某種調度算法來決定什麼時候中止一個進程,並轉而爲另外一個進程提供服務。markdown

單核CPU雙進程的狀況 2網絡

進程根據特定的調度機制和遇到I/O中斷等狀況下,進行上下文切換,輪流使用CPU資源多線程

雙核CPU雙進程的狀況 3

每個進程獨佔一個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)中均擁有惟一的地址空間

什麼是線程

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

3

系統利用PCB來完成對進程的控制和管理。一樣,系統爲線程分配一個線程控制塊TCB(Thread Control Block),將全部用於控制和管理線程的信息記錄在線程的控制塊中,TCB中一般包括:

線程標誌符 一組寄存器 線程運行狀態 優先級 線程專有存儲區 信號屏蔽

和進程同樣,線程一樣至少具備五種狀態:初始態、就緒狀態、等待(阻塞)狀態、執行狀態和終止狀態

線程之間的切換和進程同樣也須要上下文切換,這裏再也不贅述。

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

進程是資源的分配和調度的獨立單元。進程擁有完整的虛擬地址空間,當發生進程切換時,不一樣的進程擁有不一樣的虛擬地址空間。而同一進程的多個線程共享同一地址空間(不一樣進程之間的線程沒法共享) 線程是CPU調度的基本單元,一個進程包含若干線程(至少一個線程)。 線程比進程小,基本上不擁有系統資源。線程的建立和銷燬所須要的時間比進程小不少 因爲線程之間可以共享地址空間,所以,須要考慮同步和互斥操做 一個線程的意外終止會影響整個進程的正常運行,可是一個進程的意外終止不會影響其餘的進程的運行。所以,多進程程序安全性更高。 總之,多進程程序安全性高,進程切換開銷大,效率低;多線程程序維護成本高,線程切換開銷小,效率高。(python的多線程是僞多線程,下文中將詳細介紹)

什麼是協程

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

協程能夠比做子程序,但執行過程當中,子程序內部可中斷,而後轉而執行別的子程序,在適當的時候再返回來繼續執行。協程之間的切換不須要涉及任何系統調用或任何阻塞調用 協程只在一個線程中執行,是子程序之間的切換,發生在用戶態上。並且,線程的阻塞狀態是由操做系統內核來完成,發生在內核態上,所以協程相比線程節省了線程建立和切換的開銷 協程中不存在同時寫變量衝突,所以,也就不須要用來守衛關鍵區塊的同步性原語,好比互斥鎖、信號量等,而且不須要來自操做系統的支持。

協程適用於IO阻塞且須要大量併發的場景,當發生IO阻塞,由協程的調度器進行調度,經過將數據流yield掉,而且記錄當前棧上的數據,阻塞完後馬上再經過線程恢復協程棧,並把阻塞的結果放到這個線程上去運行。

6

下面,將針對在不一樣的應用場景中如何選擇使用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時,會將對象看成垃圾進行回收。(有關Python內存管理機制的相關內容能夠參見面試必備:Python內存管理機制)設想這樣一種場景:

一個進程中含有兩個線程,分別爲線程0和線程1,兩個線程全都引用對象a。

當兩個線程同時對a發生引用(並未修改,不須要使用同步性原語),就會發生同時修改對象a的引用計數器,形成引用計數少於實質性的引用,當進行垃圾回收時,形成內存異常錯誤。所以,須要一把全局鎖(即爲GIL)來保證對象引用計數的正確性和安全性。

不管是單核仍是多核,一個進程永遠只能同時執行一個線程(拿到 GIL 的線程才能執行,以下圖所示),這就是爲何在多核CPU上,Python 的多線程性能不高的根本緣由。

6

那是否是在Python中遇到併發的需求就使用多進程就萬事大吉了?其實否則,軟件工程中有一句名言:沒有銀彈!

什麼時候用?

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

CPU密集型:程序須要佔用CPU進行大量的運算和數據處理; I/O密集型:程序中須要頻繁的進行I/O操做;例如網絡中socket數據傳輸和讀取等; CPU密集+I/O密集:以上兩種的結合 CPU密集型的狀況能夠對比上面Python中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繼續執行 9

與進程的執行模式類似,彌補了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密集: 多進程+協程

※更多文章和資料|點擊後方文字直達 ↓↓↓ 100GPython自學資料包 阿里雲K8s實戰手冊 [阿里雲CDN排坑指南]CDN ECS運維指南 DevOps實踐手冊 Hadoop大數據實戰手冊 Knative雲原生應用開發指南 OSS 運維實戰手冊 雲原生架構白皮書 Zabbix企業級分佈式監控系統源碼文檔 雲原生基礎入門手冊 10G大廠面試題戳領

相關文章
相關標籤/搜索