溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

發(fā)布時(shí)間:2022-01-18 09:37:25 來(lái)源:億速云 閱讀:153 作者:iii 欄目:云計(jì)算

這篇文章主要講解了“怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程”吧!

什么是進(jìn)程

進(jìn)程-操作系統(tǒng)提供的抽象概念,是系統(tǒng)進(jìn)行資源分配和調(diào)度的基本單位,是操作系統(tǒng)結(jié)構(gòu)的基礎(chǔ)。程序是指令、數(shù)據(jù)及其組織形式的描述,進(jìn)程是程序的實(shí)體。程序本身是沒(méi)有生命周期的,它只是存在磁盤(pán)上的一些指令,程序一旦運(yùn)行就是進(jìn)程。

當(dāng)程序需要運(yùn)行時(shí),操作系統(tǒng)將代碼和所有靜態(tài)數(shù)據(jù)記載到內(nèi)存和進(jìn)程的地址空間(每個(gè)進(jìn)程都擁有唯一的地址空間,見(jiàn)下圖所示)中,通過(guò)創(chuàng)建和初始化棧(局部變量,函數(shù)參數(shù)和返回地址)、分配堆內(nèi)存以及與IO相關(guān)的任務(wù),當(dāng)前期準(zhǔn)備工作完成,啟動(dòng)程序,OS將CPU的控制權(quán)轉(zhuǎn)移到新創(chuàng)建的進(jìn)程,進(jìn)程開(kāi)始運(yùn)行。

怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

操作系統(tǒng)對(duì)進(jìn)程的控制和管理通過(guò)PCB(Processing Control Block),PCB通常是系統(tǒng)內(nèi)存占用區(qū)中的一個(gè)連續(xù)存區(qū),它存放著操作系統(tǒng)用于描述進(jìn)程情況及控制進(jìn)程運(yùn)行所需的全部信息(包括:進(jìn)程標(biāo)識(shí)號(hào),進(jìn)程狀態(tài),進(jìn)程優(yōu)先級(jí),文件系統(tǒng)指針以及各個(gè)寄存器的內(nèi)容等),進(jìn)程的PCB是系統(tǒng)感知進(jìn)程的唯一實(shí)體。

一個(gè)進(jìn)程至少具有5種基本狀態(tài):初始態(tài)、就緒狀態(tài)、等待(阻塞)狀態(tài)、執(zhí)行狀態(tài)、終止?fàn)顟B(tài)。

初始狀態(tài):進(jìn)程剛被創(chuàng)建,由于其他進(jìn)程正占有CPU資源,所以得不到執(zhí)行,只能處于初始狀態(tài)。 就緒狀態(tài):只有處于就緒狀態(tài)的經(jīng)過(guò)調(diào)度才能到執(zhí)行狀態(tài) 等待狀態(tài):進(jìn)程等待某件事件完成 執(zhí)行狀態(tài):任意時(shí)刻處于執(zhí)行狀態(tài)的進(jìn)程只能有一個(gè)(對(duì)于單核CPU來(lái)講)。 停止?fàn)顟B(tài):進(jìn)程結(jié)束

進(jìn)程間的切換

無(wú)論是在多核還是單核系統(tǒng)中,一個(gè)CPU看上去都像是在并發(fā)的執(zhí)行多個(gè)進(jìn)程,這是通過(guò)處理器在進(jìn)程間切換來(lái)實(shí)現(xiàn)的。 操作系統(tǒng)對(duì)把CPU控制權(quán)在不同進(jìn)程之間交換執(zhí)行的機(jī)制稱(chēng)為上下文切換(context switch),即保存當(dāng)前進(jìn)程的上下文,恢復(fù)新進(jìn)程的上下文,然后將CPU控制權(quán)轉(zhuǎn)移到新進(jìn)程,新進(jìn)程就會(huì)從上次停止的地方開(kāi)始。因此,進(jìn)程是輪流使用CPU的,CPU被若干進(jìn)程共享,使用某種調(diào)度算法來(lái)決定何時(shí)停止一個(gè)進(jìn)程,并轉(zhuǎn)而為另一個(gè)進(jìn)程提供服務(wù)。

單核CPU雙進(jìn)程的情況

  怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

進(jìn)程根據(jù)特定的調(diào)度機(jī)制和遇到I/O中斷等情況下,進(jìn)行上下文切換,輪流使用CPU資源

雙核CPU雙進(jìn)程的情況

怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

每一個(gè)進(jìn)程獨(dú)占一個(gè)CPU核心資源,在處理I/O請(qǐng)求的時(shí)候,CPU處于阻塞狀態(tài)

進(jìn)程間數(shù)據(jù)共享

系統(tǒng)中的進(jìn)程與其他進(jìn)程共享CPU和主存資源,為了更好的管理主存,操作系統(tǒng)提供了一種對(duì)主存的抽象概念,即為虛擬存儲(chǔ)器(VM)。它也是一個(gè)抽象的概念,它為每一個(gè)進(jìn)程提供了一個(gè)假象,即每個(gè)進(jìn)程都在獨(dú)占地使用主存。

虛擬存儲(chǔ)器主要提供了三個(gè)能力: 

將主存看成是一個(gè)存儲(chǔ)在磁盤(pán)上的高速緩存,在主存中只保存活動(dòng)區(qū)域,并根據(jù)需要在磁盤(pán)和主存之間來(lái)回傳送數(shù)據(jù),通過(guò)這種方式,更高效地使用主存 為每個(gè)進(jìn)程提供一致的地址空間,從而簡(jiǎn)化存儲(chǔ)器管理 保護(hù)每個(gè)進(jìn)程的地址空間不被其他進(jìn)程破壞 由于進(jìn)程擁有自己獨(dú)占的虛擬地址空間,CPU通過(guò)地址翻譯將虛擬地址轉(zhuǎn)換成真實(shí)的物理地址,每個(gè)進(jìn)程只能訪問(wèn)自己的地址空間。因此,在沒(méi)有其他機(jī)制(進(jìn)程間通信)的輔助下,進(jìn)程之間是無(wú)法共享數(shù)據(jù)的

以python中多進(jìn)程(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,)) # 測(cè)試多進(jìn)程使用
        # p = threading.Thread(target=count, args=(i,))  # 測(cè)試多線程使用
        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))

結(jié)果

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在進(jìn)程p{0,1,2,3,4}和主進(jìn)程(main)中均擁有唯一的地址空間

什么是線程

線程-也是操作系統(tǒng)提供的抽象概念,是程序執(zhí)行中一個(gè)單一的順序控制流程,是程序執(zhí)行流的最小單元,是處理器調(diào)度和分派的基本單位。一個(gè)進(jìn)程可以有一個(gè)或多個(gè)線程,同一進(jìn)程中的多個(gè)線程將共享該進(jìn)程中的全部系統(tǒng)資源,如虛擬地址空間,文件描述符和信號(hào)處理等等。但同一進(jìn)程中的多個(gè)線程有各自的調(diào)用棧和線程本地存儲(chǔ)(如下圖所示)。

怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

系統(tǒng)利用PCB來(lái)完成對(duì)進(jìn)程的控制和管理。同樣,系統(tǒng)為線程分配一個(gè)線程控制塊TCB(Thread Control Block),將所有用于控制和管理線程的信息記錄在線程的控制塊中,TCB中通常包括:

線程標(biāo)志符 一組寄存器 線程運(yùn)行狀態(tài) 優(yōu)先級(jí) 線程專(zhuān)有存儲(chǔ)區(qū) 信號(hào)屏蔽

和進(jìn)程一樣,線程同樣至少具有五種狀態(tài):初始態(tài)、就緒狀態(tài)、等待(阻塞)狀態(tài)、執(zhí)行狀態(tài)和終止?fàn)顟B(tài)

線程之間的切換和進(jìn)程一樣也需要上下文切換,這里不再贅述。

進(jìn)程和線程之間有許多相似的地方,那它們之間到底有什么區(qū)別呢? 進(jìn)程 VS 線程

進(jìn)程是資源的分配和調(diào)度的獨(dú)立單元。進(jìn)程擁有完整的虛擬地址空間,當(dāng)發(fā)生進(jìn)程切換時(shí),不同的進(jìn)程擁有不同的虛擬地址空間。而同一進(jìn)程的多個(gè)線程共享同一地址空間(不同進(jìn)程之間的線程無(wú)法共享) 線程是CPU調(diào)度的基本單元,一個(gè)進(jìn)程包含若干線程(至少一個(gè)線程)。 線程比進(jìn)程小,基本上不擁有系統(tǒng)資源。線程的創(chuàng)建和銷(xiāo)毀所需要的時(shí)間比進(jìn)程小很多 由于線程之間能夠共享地址空間,因此,需要考慮同步和互斥操作 一個(gè)線程的意外終止會(huì)影響整個(gè)進(jìn)程的正常運(yùn)行,但是一個(gè)進(jìn)程的意外終止不會(huì)影響其他的進(jìn)程的運(yùn)行。因此,多進(jìn)程程序安全性更高。 總之,多進(jìn)程程序安全性高,進(jìn)程切換開(kāi)銷(xiāo)大,效率低;多線程程序維護(hù)成本高,線程切換開(kāi)銷(xiāo)小,效率高。(python的多線程是偽多線程,下文中將詳細(xì)介紹)

什么是協(xié)程

協(xié)程(Coroutine,又稱(chēng)微線程)是一種比線程更加輕量級(jí)的存在,協(xié)程不是被操作系統(tǒng)內(nèi)核所管理,而完全是由程序所控制。協(xié)程與線程以及進(jìn)程的關(guān)系見(jiàn)下圖所示。

協(xié)程可以比作子程序,但執(zhí)行過(guò)程中,子程序內(nèi)部可中斷,然后轉(zhuǎn)而執(zhí)行別的子程序,在適當(dāng)?shù)臅r(shí)候再返回來(lái)繼續(xù)執(zhí)行。協(xié)程之間的切換不需要涉及任何系統(tǒng)調(diào)用或任何阻塞調(diào)用 協(xié)程只在一個(gè)線程中執(zhí)行,是子程序之間的切換,發(fā)生在用戶(hù)態(tài)上。而且,線程的阻塞狀態(tài)是由操作系統(tǒng)內(nèi)核來(lái)完成,發(fā)生在內(nèi)核態(tài)上,因此協(xié)程相比線程節(jié)省了線程創(chuàng)建和切換的開(kāi)銷(xiāo) 協(xié)程中不存在同時(shí)寫(xiě)變量沖突,因此,也就不需要用來(lái)守衛(wèi)關(guān)鍵區(qū)塊的同步性原語(yǔ),比如互斥鎖、信號(hào)量等,并且不需要來(lái)自操作系統(tǒng)的支持。

協(xié)程適用于IO阻塞且需要大量并發(fā)的場(chǎng)景,當(dāng)發(fā)生IO阻塞,由協(xié)程的調(diào)度器進(jìn)行調(diào)度,通過(guò)將數(shù)據(jù)流yield掉,并且記錄當(dāng)前棧上的數(shù)據(jù),阻塞完后立刻再通過(guò)線程恢復(fù)協(xié)程棧,并把阻塞的結(jié)果放到這個(gè)線程上去運(yùn)行。

怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

下面,將針對(duì)在不同的應(yīng)用場(chǎng)景中如何選擇使用Python中的進(jìn)程,線程,協(xié)程進(jìn)行分析。

如何選擇?

在針對(duì)不同的場(chǎng)景對(duì)比三者的區(qū)別之前,首先需要介紹一下python的多線程(一直被程序員所詬病,認(rèn)為是"假的"多線程)。

那為什么認(rèn)為Python中的多線程是“偽”多線程呢?

更換上面multiprocessing示例中, p = multiprocessing.Process(target=count, args=(i,))為p = threading.Thread(target=count, args=(i,)),其他代碼不變,運(yùn)行結(jié)果如下:

為了減少代碼冗余和文章篇幅,命名和打印不規(guī)則問(wèn)題請(qǐng)忽略

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的打印結(jié)果與線程相等,證明了線程之間是數(shù)據(jù)共享

但是,為什么多線程運(yùn)行時(shí)間比多進(jìn)程還要長(zhǎng)?這與我們上面所說(shuō)(線程的開(kāi)銷(xiāo)<<進(jìn)程的開(kāi)銷(xiāo))的事實(shí)嚴(yán)重不相符。這就要輪到Cpython(python的默認(rèn)解釋器)中GIL(Global Interpreter Lock,全局解釋鎖)登場(chǎng)了。

什么是GIL

GIL來(lái)源于Python設(shè)計(jì)之初的考慮,為了數(shù)據(jù)安全(由于內(nèi)存管理機(jī)制中采用引用計(jì)數(shù))所做的決定。某個(gè)線程想要執(zhí)行,必須先拿到 GIL。因此,可以把 GIL 看作是“通行證”,并且在一個(gè) Python進(jìn)程中,GIL 只有一個(gè),拿不到通行證的線程,就不允許進(jìn)入 CPU 執(zhí)行。 Cpython解釋器在內(nèi)存管理中采用引用計(jì)數(shù),當(dāng)對(duì)象的引用次數(shù)為0時(shí),會(huì)將對(duì)象當(dāng)作垃圾進(jìn)行回收。(有關(guān)Python內(nèi)存管理機(jī)制的相關(guān)內(nèi)容可以參見(jiàn)面試必備:Python內(nèi)存管理機(jī)制)設(shè)想這樣一種場(chǎng)景:

一個(gè)進(jìn)程中含有兩個(gè)線程,分別為線程0和線程1,兩個(gè)線程全都引用對(duì)象a。

當(dāng)兩個(gè)線程同時(shí)對(duì)a發(fā)生引用(并未修改,不需要使用同步性原語(yǔ)),就會(huì)發(fā)生同時(shí)修改對(duì)象a的引用計(jì)數(shù)器,造成引用計(jì)數(shù)少于實(shí)質(zhì)性的引用,當(dāng)進(jìn)行垃圾回收時(shí),造成內(nèi)存異常錯(cuò)誤。因此,需要一把全局鎖(即為GIL)來(lái)保證對(duì)象引用計(jì)數(shù)的正確性和安全性。

無(wú)論是單核還是多核,一個(gè)進(jìn)程永遠(yuǎn)只能同時(shí)執(zhí)行一個(gè)線程(拿到 GIL 的線程才能執(zhí)行,如下圖所示),這就是為什么在多核CPU上,Python 的多線程性能不高的根本原因。

怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

那是不是在Python中遇到并發(fā)的需求就使用多進(jìn)程就萬(wàn)事大吉了?其實(shí)不然,軟件工程中有一句名言:沒(méi)有銀彈!

何時(shí)用?

常見(jiàn)的應(yīng)用場(chǎng)景不外乎三種:

CPU密集型:程序需要占用CPU進(jìn)行大量的運(yùn)算和數(shù)據(jù)處理; I/O密集型:程序中需要頻繁的進(jìn)行I/O操作;例如網(wǎng)絡(luò)中socket數(shù)據(jù)傳輸和讀取等; CPU密集+I/O密集:以上兩種的結(jié)合 CPU密集型的情況可以對(duì)比上面Python中multiprocessing和threading的例子:多進(jìn)程的性能 > 多線程的性能。

下面主要解釋一下I/O密集型的情況。與I/O設(shè)備交互,操作系統(tǒng)最常用的解決方案就是DMA。

什么是DMA

DMA(Direct Memory Access)是系統(tǒng)中的一個(gè)特殊設(shè)備,它可以協(xié)調(diào)完成內(nèi)存到設(shè)備間的數(shù)據(jù)傳輸,中間過(guò)程不需要CPU介入。 以文件寫(xiě)入為例:

進(jìn)程p1發(fā)出數(shù)據(jù)寫(xiě)入磁盤(pán)文件的請(qǐng)求 CPU處理寫(xiě)入請(qǐng)求,通過(guò)編程告訴DMA引擎數(shù)據(jù)在內(nèi)存的位置,要寫(xiě)入數(shù)據(jù)的大小以及目標(biāo)設(shè)備等信息 CPU處理其他進(jìn)程p2的請(qǐng)求,DMA負(fù)責(zé)將內(nèi)存數(shù)據(jù)寫(xiě)入到設(shè)備中 DMA完成數(shù)據(jù)傳輸,中斷CPU CPU從p2上下文切換到p1,繼續(xù)執(zhí)行p1

  怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

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

線程Thread0首先執(zhí)行,線程Thread1等待(GIL的存在) Thread0收到I/O請(qǐng)求,將請(qǐng)求轉(zhuǎn)發(fā)給DMA,DMA執(zhí)行請(qǐng)求 Thread1占用CPU資源,繼續(xù)執(zhí)行 CPU收到DMA的中斷請(qǐng)求,切換到Thread0繼續(xù)執(zhí)行 怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程

與進(jìn)程的執(zhí)行模式相似,彌補(bǔ)了GIL帶來(lái)的缺陷,又由于線程的開(kāi)銷(xiāo)遠(yuǎn)遠(yuǎn)小于進(jìn)程的開(kāi)銷(xiāo),因此,在IO密集型場(chǎng)景中,多線程的性能更高

實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn),下面將針對(duì)I/O密集型場(chǎng)景進(jìn)行測(cè)試。

測(cè)試

執(zhí)行代碼

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))

結(jié)果

多進(jìn)程

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

多線程的執(zhí)行效性能高于多進(jìn)程 正如上面所述,針對(duì)I/O密集型的程序,協(xié)程的執(zhí)行效率更高,因?yàn)樗浅绦蜃陨硭刂频?,這樣將節(jié)省線程創(chuàng)建和切換所帶來(lái)的開(kāi)銷(xiāo)。

以Python中asyncio并發(fā)代碼庫(kù)為依賴(lài),使用async/await語(yǔ)法進(jìn)行協(xié)程的創(chuàng)建和使用。 程序代碼

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)

結(jié)果

total time: 1.001854419708252

協(xié)程的執(zhí)行效性能高于多線程

感謝各位的閱讀,以上就是“怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)怎么用python實(shí)現(xiàn)進(jìn)程,線程和協(xié)程這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI