Python 高級(jí)教程之線程進(jìn)程和協(xié)程的代碼解析
進(jìn)程
進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序,是 CPU 的最小工作單元。
進(jìn)程 5 種基本狀態(tài)
一個(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)過調(diào)度才能到執(zhí)行狀態(tài)
- 等待狀態(tài):進(jìn)程等待某件事件完成
- 執(zhí)行狀態(tài):任意時(shí)刻處于執(zhí)行狀態(tài)的進(jìn)程只能有一個(gè)(對(duì)于單核CPU來講)。
- 停止?fàn)顟B(tài):進(jìn)程結(jié)束
進(jìn)程的特點(diǎn)
- 動(dòng)態(tài)性:進(jìn)程是程序的一次執(zhí)行過程,動(dòng)態(tài)產(chǎn)生,動(dòng)態(tài)消亡。
- 獨(dú)立性:進(jìn)程是一個(gè)能獨(dú)立運(yùn)行的基本單元。是系統(tǒng)分配資源與調(diào)度的基本單元。
- 并發(fā)性:任何進(jìn)程都可以與其他進(jìn)程并發(fā)執(zhí)行。
- 結(jié)構(gòu)性:進(jìn)程由程序、數(shù)據(jù)和進(jìn)程控制塊三部分組成。
multiprocessing 是比 fork 更高級(jí)的庫(kù),使用 multiprocessing 可以更加輕松的實(shí)現(xiàn)多進(jìn)程程序。
#!/usr/bin/env python # -*- coding:utf-8 -*- from multiprocessing import Process import threading import time def foo(i): print 'say hi',i for i in range(10): p = Process(target=foo,args=(i,)) p.start()
注意:由于進(jìn)程之間的數(shù)據(jù)需要各自持有一份,所以創(chuàng)建進(jìn)程需要的非常大的開銷。并且python不能再Windows下創(chuàng)建進(jìn)程!
使用多進(jìn)程的時(shí)候,最好是創(chuàng)建和和 CPU 核數(shù)相等的進(jìn)程數(shù)。
進(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ǔ)在磁盤上的高速緩存,在主存中只保存活動(dòng)區(qū)域,并根據(jù)需要在磁盤和主存之間來回傳送數(shù)據(jù),通過這種方式,更高效地使用主存
- 為每個(gè)進(jìn)程提供一致的地址空間,從而簡(jiǎn)化存儲(chǔ)器管理
- 保護(hù)每個(gè)進(jìn)程的地址空間不被其他進(jìn)程破壞
由于進(jìn)程擁有自己獨(dú)占的虛擬地址空間,CPU通過地址翻譯將虛擬地址轉(zhuǎn)換成真實(shí)的物理地址,每個(gè)進(jìn)程只能訪問自己的地址空間。因此,在沒有其他機(jī)制(進(jìn)程間通信)的輔助下,進(jìn)程之間是無法共享數(shù)據(jù)的。
進(jìn)程各自持有一份數(shù)據(jù),默認(rèn)無法共享數(shù)據(jù)。默認(rèn)的進(jìn)程之間相互是獨(dú)立,如果想讓進(jìn)程之間數(shù)據(jù)共享,就得有個(gè)特殊的數(shù)據(jù)結(jié)構(gòu),這個(gè)數(shù)據(jù)結(jié)構(gòu)就可以理解為他有穿墻的功能 如果你能穿墻的話兩邊就都可以使用了
#!/usr/bin/env python #coding:utf-8 from multiprocessing import Process from multiprocessing import Manager import time li = [] def foo(i): li.append(i) print 'say hi',li for i in range(10): p = Process(target=foo,args=(i,)) p.start() print 'ending',li
使用特殊的數(shù)據(jù)類型,來進(jìn)行穿墻:
#通過特殊的數(shù)據(jù)結(jié)構(gòu):數(shù)組(Array) from multiprocessing import Process,Array #創(chuàng)建一個(gè)只包含數(shù)字類型的數(shù)組(python中叫列表) #并且數(shù)組是不可變的,在C,或其他語(yǔ)言中,數(shù)組是不可變的,之后再python中數(shù)組(列表)是可以變得 #當(dāng)然其他語(yǔ)言中也提供可變的數(shù)組 #在C語(yǔ)言中數(shù)組和字符串是一樣的,如果定義一個(gè)列表,如果可以增加,那么我需要在你內(nèi)存地址后面再開辟一塊空間,那我給你預(yù)留多少呢? #在python中的list可能用鏈表來做的,我記錄了你前面和后面是誰(shuí)。列表不是連續(xù)的,數(shù)組是連續(xù)的 ''' 上面不是列表是“數(shù)組"數(shù)組是不可變的,附加內(nèi)容是為了更好的理解數(shù)組! ''' temp = Array('i', [11,22,33,44]) #這里的i是C語(yǔ)言中的數(shù)據(jù)結(jié)構(gòu),通過他來定義你要共享的內(nèi)容的類型!點(diǎn)進(jìn)去看~ def Foo(i): temp[i] = 100+i for item in temp: print i,'----->',item for i in range(2): p = Process(target=Foo,args=(i,)) p.start() 第二種方法: #方法二:manage.dict()共享數(shù)據(jù) from multiprocessing import Process,Manager #這個(gè)特殊的數(shù)據(jù)類型Manager manage = Manager() dic = manage.dict() #這里調(diào)用的時(shí)候,使用字典,這個(gè)字典和咱們python使用方法是一樣的! def Foo(i): dic[i] = 100+i print dic.values() for i in range(2): p = Process(target=Foo,args=(i,)) p.start() p.join()
既然進(jìn)程之間可以進(jìn)行共享數(shù)據(jù),如果多個(gè)進(jìn)程同時(shí)修改這個(gè)數(shù)據(jù)是不是就會(huì)造成臟數(shù)據(jù)?是不是就得需要鎖!
進(jìn)程的鎖和線程的鎖使用方式是非常一樣的知識(shí)他們是用的類是在不同地方的。
進(jìn)程池
進(jìn)程池內(nèi)部維護(hù)一個(gè)進(jìn)程序列,當(dāng)使用時(shí),則去進(jìn)程池中獲取一個(gè)進(jìn)程,如果進(jìn)程池序列中沒有可供使用的進(jìn)進(jìn)程,那么程序就會(huì)等待,直到進(jìn)程池中有可用進(jìn)程為止。
進(jìn)程池中有兩個(gè)方法:
- apply
- apply_async
#!/usr/bin/env python # -*- coding:utf-8 -*- from multiprocessing import Process,Pool import time def Foo(i): time.sleep(2) return i+100 def Bar(arg): print arg pool = Pool(5) #創(chuàng)建一個(gè)進(jìn)程池 #print pool.apply(Foo,(1,))#去進(jìn)程池里去申請(qǐng)一個(gè)進(jìn)程去執(zhí)行Foo方法 #print pool.apply_async(func =Foo, args=(1,)).get() for i in range(10): pool.apply_async(func=Foo, args=(i,),callback=Bar) print 'end' pool.close() pool.join()#進(jìn)程池中進(jìn)程執(zhí)行完畢后再關(guān)閉,如果注釋,那么程序直接關(guān)閉。 ''' apply 主動(dòng)的去執(zhí)行 pool.apply_async(func=Foo, args=(i,),callback=Bar) 相當(dāng)于異步,當(dāng)申請(qǐng)一個(gè)線程之后,執(zhí)行FOO方法就不管了,執(zhí)行完之后就在執(zhí)行callback ,當(dāng)你執(zhí)行完之后,在執(zhí)行一個(gè)方法告訴我執(zhí)行完了 callback 有個(gè)函數(shù),這個(gè)函數(shù)就是操作的Foo函數(shù)的返回值! '''
進(jìn)程的缺點(diǎn)
無法即時(shí)完成的任務(wù)帶來大量的上下文切換代價(jià)與時(shí)間代價(jià)。
進(jìn)程的上下文:當(dāng)一個(gè)進(jìn)程在執(zhí)行時(shí),CPU的所有寄存器中的值、進(jìn)程的狀態(tài)以及堆棧中的內(nèi)容被稱為該進(jìn)程的上下文。
上下文切換:當(dāng)內(nèi)核需要切換到另一個(gè)進(jìn)程時(shí),它需要保存當(dāng)前進(jìn)程的所有狀態(tài),即保存當(dāng)前進(jìn)程的上下文,以便在再次執(zhí)行該進(jìn)程時(shí),能夠得到切換時(shí)的狀態(tài)并執(zhí)行下去。
線程
線程的定義
在計(jì)算中,進(jìn)程是正在執(zhí)行的計(jì)算機(jī)程序的一個(gè)實(shí)例。任何進(jìn)程都有 3 個(gè)基本組成部分:
- 一個(gè)可執(zhí)行程序。
- 程序所需的相關(guān)數(shù)據(jù)(變量、工作空間、緩沖區(qū)等)
- 程序的執(zhí)行上下文(進(jìn)程狀態(tài))
線程是進(jìn)程中可以調(diào)度執(zhí)行的實(shí)體。此外,它是可以在 OS(操作系統(tǒng))中執(zhí)行的最小處理單元。
簡(jiǎn)而言之,線程是程序中的一系列此類指令,可以獨(dú)立于其他代碼執(zhí)行。為簡(jiǎn)單起見,您可以假設(shè)線程只是進(jìn)程的子集!
線程在線程控制塊 (TCB)中包含所有這些信息:
- 線程標(biāo)識(shí)符:為每個(gè)新線程分配唯一 id (TID)
- 堆棧指針:指向進(jìn)程中線程的堆棧。堆棧包含線程范圍內(nèi)的局部變量。
- 程序計(jì)數(shù)器:存放線程當(dāng)前正在執(zhí)行的指令地址的寄存器。
- 線程狀態(tài):可以是running、ready、waiting、start或done。
- 線程的寄存器集:分配給線程進(jìn)行計(jì)算的寄存器。
- 父進(jìn)程指針:指向線程所在進(jìn)程的進(jìn)程控制塊 (PCB) 的指針。
多線程被定義為處理器同時(shí)執(zhí)行多個(gè)線程的能力。
在一個(gè)簡(jiǎn)單的單核 CPU 中,它是通過線程之間的頻繁切換來實(shí)現(xiàn)的。這稱為上下文切換。在上下文切換中,只要發(fā)生任何中斷(由于 I/O
或手動(dòng)設(shè)置),就會(huì)保存一個(gè)線程的狀態(tài)并加載另一個(gè)線程的狀態(tài)。上下文切換發(fā)生得如此頻繁,以至于所有線程似乎都在并行運(yùn)行(這被稱為多任務(wù))。
在 Python 中,threading模塊提供了一個(gè)非常簡(jiǎn)單直觀的 API,用于在程序中生成多個(gè)線程。
使用線程模塊的簡(jiǎn)單示例
讓我們考慮一個(gè)使用線程模塊的簡(jiǎn)單示例:
# Python程序說明線程的概念 # 導(dǎo)入線程模塊 import threading def print_cube(num): """ 打印給定數(shù)字立方的函數(shù) """ print("立方: {}".format(num * num * num)) def print_square(num): """ 打印給定數(shù)字平方的函數(shù) """ print("平方: {}".format(num * num)) if __name__ == "__main__": # creating thread t1 = threading.Thread(target=print_square, args=(10,)) t2 = threading.Thread(target=print_cube, args=(10,)) # starting thread 1 t1.start() # starting thread 2 t2.start() # 等到線程 1 完全執(zhí)行 t1.join() # 等到線程 2 完全執(zhí)行 t2.join() # 兩個(gè)線程完全執(zhí)行 print("完成!")
平方: 100
立方: 1000
完成!
代碼解析
讓我們?cè)囍斫馍厦娴拇a:
- 要導(dǎo)入線程模塊,我們這樣做:
import threading
- 要?jiǎng)?chuàng)建一個(gè)新線程,我們創(chuàng)建一個(gè)Thread類的對(duì)象。它需要以下參數(shù):
- target : 線程要執(zhí)行的函數(shù)
- args:要傳遞給目標(biāo)函數(shù)的參數(shù)
在上面的示例中,我們創(chuàng)建了 2 個(gè)具有不同目標(biāo)函數(shù)的線程:
t1 = threading.Thread(target=print_square, args=(10,)) t2 = threading.Thread(target=print_cube, args=(10,))
要啟動(dòng)一個(gè)線程,我們使用 Thread 類的 start 方法。
t1.start() t2.start()
一旦線程啟動(dòng),當(dāng)前程序(你可以把它想象成一個(gè)主線程)也會(huì)繼續(xù)執(zhí)行。為了在線程完成之前停止當(dāng)前程序的執(zhí)行,我們使用join方法。
t1.join() t2.join()
結(jié)果,當(dāng)前程序?qū)⑹紫鹊却?t1 的完成,然后 t2 。一旦它們完成,則執(zhí)行當(dāng)前程序的剩余語(yǔ)句。
協(xié)程
協(xié)程(Coroutine,又稱微線程,纖程)是一種比線程更加輕量級(jí)的存在,協(xié)程不是被操作系統(tǒng)內(nèi)核所管理,而完全是由程序所控制。
我們都熟悉函數(shù),也稱為子例程、過程、子過程等。函數(shù)是打包為一個(gè)單元以執(zhí)行特定任務(wù)的指令序列。當(dāng)一個(gè)復(fù)雜函數(shù)的邏輯被分成幾個(gè)獨(dú)立的步驟,這些步驟本身就是函數(shù)時(shí),這些函數(shù)被稱為輔助函數(shù)或子程序。
Python 中的子程序由負(fù)責(zé)協(xié)調(diào)這些子程序的使用的主函數(shù)調(diào)用。子程序只有一個(gè)入口點(diǎn)。 協(xié)程是子程序的泛化。它們用于協(xié)作式多任務(wù)處理,其中一個(gè)進(jìn)程定期或在空閑時(shí)自愿放棄(放棄)控制權(quán),以使多個(gè)應(yīng)用程序能夠同時(shí)運(yùn)行。協(xié)程和子程序的區(qū)別是:
- 與子程序不同,協(xié)程有許多用于暫停和恢復(fù)執(zhí)行的入口點(diǎn)。協(xié)程可以暫停其執(zhí)行并將控制權(quán)轉(zhuǎn)移給其他協(xié)程,并且可以從中斷點(diǎn)重新開始執(zhí)行。
- 與子程序不同,沒有主函數(shù)可以按特定順序調(diào)用協(xié)程并協(xié)調(diào)結(jié)果。協(xié)程是協(xié)作的,這意味著它們鏈接在一起形成管道。一個(gè)協(xié)程可能會(huì)使用輸入數(shù)據(jù)并將其發(fā)送給其他處理它的協(xié)程。最后,可能會(huì)有一個(gè)協(xié)程來顯示結(jié)果。
協(xié)程與線程
現(xiàn)在您可能在想?yún)f(xié)程與線程有何不同,兩者似乎都在做同樣的工作。
在線程的情況下,它是根據(jù)調(diào)度程序在線程之間切換的操作系統(tǒng)(或運(yùn)行時(shí)環(huán)境)。而在協(xié)程的情況下,決定何時(shí)切換協(xié)程的是程序員和編程語(yǔ)言。協(xié)程通過程序員在設(shè)定點(diǎn)暫停和恢復(fù)來協(xié)同工作多任務(wù)。
Python 協(xié)程
在 Python 中,協(xié)程類似于生成器,但幾乎沒有額外的方法,而且我們使用yield語(yǔ)句的方式也有細(xì)微的變化。生成器為迭代生成數(shù)據(jù),而協(xié)程也可以使用數(shù)據(jù)。
在 Python 2.5 中,引入了對(duì) yield 語(yǔ)句的輕微修改,現(xiàn)在 yield 也可以用作表達(dá)式。例如在作業(yè)的右側(cè)——
line = (yield)
我們發(fā)送給協(xié)程的任何值都會(huì)被(yield)表達(dá)式捕獲并返回。
可以通過send()方法將值發(fā)送到協(xié)程。例如,考慮這個(gè)協(xié)程,它打印出帶有前綴“Dear”的名稱。我們將使用 send() 方法將名稱發(fā)送到協(xié)程。
# 用于演示協(xié)程執(zhí)行的 Python3 程序 def print_name(prefix): print("Searching prefix:{}".format(prefix)) while True: name = (yield) if prefix in name: print(name) # 調(diào)用協(xié)程,什么都不會(huì)發(fā)生 corou = print_name("Dear") # 這將開始執(zhí)行協(xié)程并打印第一行 "Searching prefix..." # 并將執(zhí)行推進(jìn)到第一個(gè) yield 表達(dá)式 corou.__next__() # 發(fā)送輸入 corou.send("Haiyong") corou.send("Dear Haiyong")
輸出:
Searching prefix:Dear
Dear Haiyong
協(xié)程的執(zhí)行
協(xié)程的執(zhí)行類似于生成器。當(dāng)我們調(diào)用協(xié)程時(shí),什么都沒有發(fā)生,它只在響應(yīng)next()
和send ()
方法時(shí)運(yùn)行。在上面的例子中可以清楚地看到這一點(diǎn),因?yàn)橹挥性谡{(diào)用__next__()
方法之后,我們的協(xié)程才開始執(zhí)行。在這個(gè)調(diào)用之后,執(zhí)行前進(jìn)到第一個(gè) yield 表達(dá)式,現(xiàn)在執(zhí)行暫停并等待值被發(fā)送到 corou 對(duì)象。當(dāng)?shù)谝粋€(gè)值被發(fā)送給它時(shí),它會(huì)檢查前綴和打印名稱(如果存在前綴)。打印完名稱后,它會(huì)遍歷循環(huán),直到再次遇到name = (yield)表達(dá)式。
關(guān)閉協(xié)程
協(xié)程可能無限期運(yùn)行,關(guān)閉協(xié)程使用close()
方法。當(dāng)協(xié)程關(guān)閉時(shí),它會(huì)生成GeneratorExit
異常,該異??梢砸酝ǔ2东@的方式捕獲。關(guān)閉協(xié)程后,如果我們嘗試發(fā)送值,它將引發(fā)StopIteration
異常。下面是一個(gè)簡(jiǎn)單的例子:
# Python3 program for demonstrating # closing a coroutine def print_name(prefix): print("Searching prefix:{}".format(prefix)) try : while True: name = (yield) if prefix in name: print(name) except GeneratorExit: print("關(guān)閉協(xié)程!!") corou = print_name("Dear") corou.__next__() corou.send("Haiyong") corou.send("Dear Haiyong") corou.close()
輸出:
搜索前綴:Dear
Dear Haiyong
關(guān)閉協(xié)程?。?/p>
鏈接協(xié)程以創(chuàng)建管道
協(xié)程可用于設(shè)置管道。我們可以使用 send() 方法將協(xié)程鏈接在一起并通過管道推送數(shù)據(jù)。管道需要:
- 初始源(生產(chǎn)者)派生整個(gè)管道。生產(chǎn)者通常不是協(xié)程,它只是一個(gè)簡(jiǎn)單的方法。
- 一個(gè) sink,它是管道的端點(diǎn)。接收器可能會(huì)收集所有數(shù)據(jù)并顯示它。
以下是一個(gè)簡(jiǎn)單的鏈接示例
# 用于演示協(xié)程鏈接的 Python 程序 def producer(sentence, next_coroutine): ''' producer 只是拆分字符串并將其 提供給 pattern_filter 協(xié)程 tokens = sentence.split(" ") for token in tokens: next_coroutine.send(token) next_coroutine.close() def pattern_filter(pattern="ing", next_coroutine=None): 在接收到的令牌中搜索模式,如果模式匹配, 將其發(fā)送到 print_token() 協(xié)程進(jìn)行打印 print("Searching for {}".format(pattern)) try: while True: token = (yield) if pattern in token: next_coroutine.send(token) except GeneratorExit: print("過濾完成!!") def print_token(): 充當(dāng)接收器,只需打印接收到的令牌 print("我沉了,我會(huì)打印令牌") print(token) print("打印完成!") pt = print_token() pt.__next__() pf = pattern_filter(next_coroutine = pt) pf.__next__() sentence = "Haiyong is running behind a fast moving car" producer(sentence, pf)
輸出:
我沉了,我會(huì)打印令牌
Searching for ing
running
moving
過濾完成!
打印完成!
總結(jié)
1.線程和協(xié)程推薦在 IO 密集型的任務(wù)(比如網(wǎng)絡(luò)調(diào)用)中使用,而在CPU密集型的任務(wù)中,表現(xiàn)較差。
2.對(duì)于CPU密集型的任務(wù),則需要多個(gè)進(jìn)程,繞開GIL的限制,利用所有可用的CPU核心,提高效率。
3.在高并發(fā)下的最佳實(shí)踐就是多進(jìn)程+協(xié)程,既充分利用多核,又充分發(fā)揮協(xié)程的高效率,可獲得極高的性能。
- CPU 密集型: 多進(jìn)程
- IO 密集型: 多線程(協(xié)程維護(hù)成本較高,而且在讀寫文件方面效率沒有顯著提升)
- CPU 密集和 IO 密集: 多進(jìn)程+協(xié)程
到此這篇關(guān)于Python 高級(jí)教程之線程進(jìn)程和協(xié)程的代碼解析的文章就介紹到這了,更多相關(guān)Python線程進(jìn)程和協(xié)程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python?4種實(shí)現(xiàn)定時(shí)任務(wù)的方案
這篇文章主要給大家分享了Python?4種實(shí)現(xiàn)定時(shí)任務(wù)的方案,運(yùn)用 while True: + sleep()、Timeloop 庫(kù)、threading.Timer 、內(nèi)置模塊 sched ,下面就來看看具體的實(shí)現(xiàn)過程吧2021-12-12Django實(shí)現(xiàn)前臺(tái)上傳并顯示圖片功能
這篇文章主要介紹了Django實(shí)現(xiàn)前臺(tái)上傳并顯示圖片功能,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05將.py文件轉(zhuǎn)化為.exe文件的詳細(xì)過程
學(xué)Python那么久了,才知道自己不會(huì)把腳本編譯成可執(zhí)行exe文件,下面這篇文章主要給大家介紹了關(guān)于將.py文件轉(zhuǎn)化為.exe文件的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09在Django的form中使用CSS進(jìn)行設(shè)計(jì)的方法
這篇文章主要介紹了在Django的form中使用CSS進(jìn)行設(shè)計(jì)的方法,Django是Python重多人氣開發(fā)框架中最為著名的一個(gè),需要的朋友可以參考下2015-07-07Python實(shí)現(xiàn)求解斐波那契第n項(xiàng)的解法(包括矩陣乘法+快速冪)
這篇文章主要介紹怎么使用Python求解斐波那契第n項(xiàng),方法多樣,邏輯清晰,代碼簡(jiǎn)單詳細(xì),有這方面需要的朋友可以參考下2021-04-04