Python-并發(fā)編程

什么是進(jìn)程

  • 進(jìn)程:一個程序的執(zhí)行過程或者一個任務(wù)。
  • 并發(fā): 是偽并行,看起來是同時運行,其實通過單個cpu+多道技術(shù)就可以實現(xiàn)并發(fā)。
  • 并行: 同時運行,只有具備多個cpu才能實現(xiàn)并行。

新進(jìn)程的創(chuàng)建都是由一個已經(jīng)存在的進(jìn)程執(zhí)行了一個用于創(chuàng)建進(jìn)程的系統(tǒng)調(diào)用而創(chuàng)建的:

  1. 在UNIX中系統(tǒng)調(diào)用是:fork。 fork 會創(chuàng)建和父進(jìn)程一模一樣的副本,在初始狀態(tài),二者有相同的存儲映像和數(shù)據(jù),同樣的環(huán)境字符串和同樣的打開文件(在shell解釋器進(jìn)程中,執(zhí)行一個命令就會創(chuàng)建一個子進(jìn)程)

  2. 在windows中,該系統(tǒng)調(diào)用的是 CreateProcess, CreateProcess既處理進(jìn)程的創(chuàng)建,也負(fù)責(zé)把正確的程序裝入新的進(jìn)程。

關(guān)于創(chuàng)建進(jìn)程,UNIX和Windows:

  1. 相同的是:進(jìn)程創(chuàng)建后,父進(jìn)程和子進(jìn)程有各自不同的內(nèi)存地址空間(多道技術(shù)要求物理層面實現(xiàn)進(jìn)程之間的內(nèi)存隔離),任何一個進(jìn)程修改自身的空間數(shù)據(jù)都不會影響到另一個進(jìn)程。

  2. 不同的是:在UNIX 中,子進(jìn)程的初始地址空間是父進(jìn)程的一個副本,子進(jìn)程和父進(jìn)程是可以有只讀的共享存儲區(qū)。但是在Windows中,從一開始,父進(jìn)程和子進(jìn)程的地址空間就是不同的。

開啟進(jìn)程的兩種方式

方式一

開啟進(jìn)程(使用提供的模塊):

# coding=utf-8

from multiprocessing import Process  # 進(jìn)程模塊
import time

def task(name):
    print('%s is running' %name)
    time.sleep(2)
    print('%s is done' %name)

if __name__ == '__main__':    # 在Windows系統(tǒng)上,進(jìn)程開啟前需要先定義進(jìn)程執(zhí)行函數(shù),進(jìn)程的執(zhí)行代碼必須放在開啟進(jìn)程代碼前
    p=Process(target=task,args=('cat',))  # target 指定進(jìn)程函數(shù),args指定參數(shù),參數(shù)必須為元組
    p.start()
    print('主')

輸出結(jié)果:

主
cat is running
cat is done

方式二

自定義進(jìn)程:

# coding=utf-8
from multiprocessing import Process
import time

class MyProcess(Process):   # 自定義類,繼承系統(tǒng)的Process
    def __init__(self,name):
        super(MyProcess,self).__init__()
        self.name=name

    def run(self):
        print('%s is running' %self.name)
        time.sleep(2)
        print('%s is done' %self.name)

if __name__ == '__main__':
    p=MyProcess('進(jìn)程1')
    p.start()    # 調(diào)用 run() 函數(shù)
    print('主')

輸出結(jié)果:

主
進(jìn)程1 is running
進(jìn)程1 is done

進(jìn)程的隔離

創(chuàng)建進(jìn)程的過程中,子進(jìn)程在初始狀態(tài)會拷貝父進(jìn)程中的數(shù)據(jù),但是在自己運行過程中,修改子進(jìn)程自己的數(shù)據(jù)不會影響父進(jìn)程的數(shù)據(jù),進(jìn)程之間的數(shù)據(jù)是相互隔離的:

# coding=utf-8

from multiprocessing import Process  # 進(jìn)程模塊
import time
n = 100
def task(name):
    global n
    n = 0
    time.sleep(2)
    print('子進(jìn)程',n)

if __name__ == '__main__':
    p=Process(target=task,args=('cat',))
    p.start()
    p.join()     # 等待子進(jìn)程運行完畢后再運行后面主進(jìn)程的代碼
    print('主進(jìn)程',n)

輸出結(jié)果:

子進(jìn)程 0
主進(jìn)程 100

進(jìn)程相關(guān)的方法

join 方法

# coding=utf-8
import os
from multiprocessing import Process  # 進(jìn)程模塊
import time

def task(n):
    print('%s is running' %os.getpid())
    time.sleep(n)
    print('%s is done' %os.getpid())

if __name__ == '__main__':
    p1 = Process(target=task,args=(1,))
    p2 = Process(target=task,args=(2,))
    p3 = Process(target=task,args=(3,))
    start_time=time.time()    # 開啟進(jìn)程計時
    p1.start()
    p2.start()
    p3.start()

    p1.join()   # 等待1s p1結(jié)束
    p2.join()   # 等待1s p2結(jié)束
    p3.join()   # 等待1s p3結(jié)束
    stop_time=time.time()     # 計時結(jié)束
    print('主進(jìn)程',(stop_time-start_time))

運行結(jié)果:

16188 is running
228 is running
13480 is running
16188 is done
228 is done
13480 is done
主進(jìn)程 3.086230516433716

可以改寫以上代碼:

# coding=utf-8
import os
from multiprocessing import Process  # 進(jìn)程模塊
import time

def task(n):
    print('%s is running' %os.getpid())
    time.sleep(n)
    print('%s is done' %os.getpid())

if __name__ == '__main__':
    p1 = Process(target=task,args=(1,))
    p2 = Process(target=task,args=(2,))
    p3 = Process(target=task,args=(3,))
    start_time=time.time()    # 開啟進(jìn)程計時

    p_list=[p1,p2,p3]
    for p in p_list:
        p.start()
    for p in p_list:
        p.join()

    stop_time=time.time()     # 計時結(jié)束
    print('主進(jìn)程',(stop_time-start_time))

如果要改為串行執(zhí)行,可以用如下方式:

# coding=utf-8
import os
from multiprocessing import Process  # 進(jìn)程模塊
import time

def task(n):
    print('%s is running' %os.getpid())
    time.sleep(n)
    print('%s is done' %os.getpid())

if __name__ == '__main__':          # 如果串行執(zhí)行,可以不調(diào)用此處的方法
    p1 = Process(target=task,args=(1,))
    p2 = Process(target=task,args=(2,))
    p3 = Process(target=task,args=(3,))
    start_time=time.time()    # 開啟進(jìn)程計時

    task(1)
    task(2)
    task(3)

    stop_time=time.time()     # 計時結(jié)束
    print('主進(jìn)程',(stop_time-start_time))

輸出結(jié)果:

12116 is running
12116 is done
12116 is running
12116 is done
12116 is running
12116 is done
主進(jìn)程 6.001911163330078

其它常見方法

  • p.start(): 啟動進(jìn)程,并調(diào)用子進(jìn)程中的p.run()
  • p.run(): 進(jìn)程啟動時運行的方法,正是它去調(diào)用target指定的函數(shù),自定義的類中一定要實現(xiàn)該方法。
  • p.terminate(): 強(qiáng)制終止進(jìn)程p,不會進(jìn)行任何清理操作,如果p 創(chuàng)建了子進(jìn)程,該子進(jìn)程就成了僵尸進(jìn)程,使用該方法需要特別小心這種情況,如果P還保存了一個鎖那么也不會被釋放,進(jìn)而導(dǎo)致死鎖。
  • p.is_alive(): 如果p仍然運行,返回True
  • p.join([timeout]): 主線程等待p終止(p處于運行的狀態(tài),主線程處于等待狀態(tài))。timeout是可選的超時時間。p.join只能join住start開啟的進(jìn)程,不能join住run開啟的進(jìn)程

進(jìn)程屬性

  • p.daemon: 默認(rèn)值為False,如果設(shè)置為True,代表p為后臺運行的守護(hù)進(jìn)程,當(dāng)p的父進(jìn)程終止時,p也隨之終止,并設(shè)定為True之后,P不能創(chuàng)建自己的新進(jìn)程,必須在p.start()之前設(shè)置
  • p.name: 進(jìn)程的名稱
  • p.pid: 進(jìn)程的pid
  • p.exitcode: 進(jìn)程在運行時為None,如果為-N,表示被信號N結(jié)束
  • p.authkey: 進(jìn)程的身份驗證鍵,默認(rèn)是由os.urandom()隨機(jī)生成的32字符的字符串。這個鍵的用途是為涉及網(wǎng)絡(luò)連接的底層進(jìn)程通信提供安全性,這類連接只有在?相同的身份驗證鍵時才能成功

僵尸進(jìn)程和孤兒進(jìn)程

僵尸進(jìn)程

在UNIX中,一個進(jìn)程使用fork創(chuàng)建子進(jìn)程,如果子進(jìn)程退出,而父進(jìn)程并沒有調(diào)用wait或waitpid獲取子進(jìn)程的狀態(tài)信息,那么子進(jìn)程的進(jìn)程描述符仍然保存在系統(tǒng)中。這種進(jìn)程稱之為僵尸進(jìn)程。詳解如下

我們知道在unix/linux中,正常情況下子進(jìn)程是通過父進(jìn)程創(chuàng)建的,子進(jìn)程在創(chuàng)建新的進(jìn)程。子進(jìn)程的結(jié)束和父進(jìn)程的運行是一個異步過程,即父進(jìn)程永遠(yuǎn)無法預(yù)測子進(jìn)程到底什么時候結(jié)束,如果子進(jìn)程一結(jié)束就立刻回收其全部資源,那么在父進(jìn)程內(nèi)將無法獲取子進(jìn)程的狀態(tài)信息。

因此,UNIX提供了一種機(jī)制可以保證父進(jìn)程可以在任意時刻獲取子進(jìn)程結(jié)束時的狀態(tài)信息:

  1. 在每個進(jìn)程退出的時候,內(nèi)核釋放該進(jìn)程所有的資源,包括打開的文件,占用的內(nèi)存等。但是仍然為其保留一定的信息(包括進(jìn)程號the process ID,退出狀態(tài)the termination status of the process,運行時間the amount of CPU time taken by the process等)
  2. 直到父進(jìn)程通過wait / waitpid來取時才釋放. 但這樣就導(dǎo)致了問題,如果進(jìn)程不調(diào)用wait / waitpid的話,那么保留的那段信息就不會釋放,其進(jìn)程號就會一直被占用,但是系統(tǒng)所能使用的進(jìn)程號是有限的,如果大量的產(chǎn)生僵死進(jìn)程,將因為沒有可用的進(jìn)程號而導(dǎo)致系統(tǒng)不能產(chǎn)生新的進(jìn)程. 此即為僵尸進(jìn)程的危害,應(yīng)當(dāng)避免。

任何一個子進(jìn)程(init除外)在exit()之后,并非馬上就消失掉,而是留下一個稱為僵尸進(jìn)程(Zombie)的數(shù)據(jù)結(jié)構(gòu),等待父進(jìn)程處理。這是每個子進(jìn)程在結(jié)束時都要經(jīng)過的階段。如果子進(jìn)程在exit()之后,父進(jìn)程沒有來得及處理,這時用ps命令就能看到子進(jìn)程的狀態(tài)是“Z”。如果父進(jìn)程能及時 處理,可能用ps命令就來不及看到子進(jìn)程的僵尸狀態(tài),但這并不等于子進(jìn)程不經(jīng)過僵尸狀態(tài)。 如果父進(jìn)程在子進(jìn)程結(jié)束之前退出,則子進(jìn)程將由init接管。init將會以父進(jìn)程的身份對僵尸狀態(tài)的子進(jìn)程進(jìn)行處理。

僵尸進(jìn)程的危害場景
例如有個進(jìn)程,它定期的產(chǎn)生一個子進(jìn)程,這個子進(jìn)程需要做的事情很少,做完它該做的事情之后就退出了,因此這個子進(jìn)程的生命周期很短,但是,父進(jìn)程只管生成新的子進(jìn)程,至于子進(jìn)程 退出之后的事情,則一概不聞不問,這樣,系統(tǒng)運行上一段時間之后,系統(tǒng)中就會存在很多的僵尸進(jìn)程,倘若用ps命令查看的話,就會看到很多狀態(tài)為Z的進(jìn)程。 嚴(yán)格地來說,僵尸進(jìn)程并不是問題的根源,罪魁禍?zhǔn)资钱a(chǎn)生出大量僵死進(jìn)程的那個父進(jìn)程。因此,當(dāng)我們尋求如何消滅系統(tǒng)中大量的僵死進(jìn)程時,答案就是把產(chǎn)生大 量僵死進(jìn)程的那個元兇槍斃掉(也就是通過kill發(fā)送SIGTERM或者SIGKILL信號啦)。槍斃了元兇進(jìn)程之后,它產(chǎn)生的僵尸進(jìn)程就變成了孤兒進(jìn) 程,這些孤兒進(jìn)程會被init進(jìn)程接管,init進(jìn)程會wait()這些孤兒進(jìn)程,釋放它們占用的系統(tǒng)進(jìn)程表中的資源,這樣,這些已經(jīng)僵死的孤兒進(jìn)程 就能瞑目而去了

在unix系統(tǒng)中,執(zhí)行top命令中 zombie的數(shù)量就是當(dāng)前僵尸進(jìn)程的數(shù)量

回收僵尸進(jìn)程的解決辦法
等待父進(jìn)程正常結(jié)束后會調(diào)用wait/waitpid去回收僵尸進(jìn)程
但如果父進(jìn)程是一個死循環(huán),永遠(yuǎn)不會結(jié)束,那么該僵尸進(jìn)程就會一直存在,僵尸進(jìn)程過多,就是有害的

  • 解決方法一:殺死父進(jìn)程
  • 解決方法二:對開啟的子進(jìn)程應(yīng)該記得使用join,join會回收僵尸進(jìn)程
  • 解決辦法三:python中有一個signal模塊,可以使用signal(SIGCHLD, SIG_IGN)處理僵尸進(jìn)程

孤兒進(jìn)程

一個父進(jìn)程退出,而它的一個或多個子進(jìn)程還在運行,那么那些子進(jìn)程將成為孤兒進(jìn)程。孤兒進(jìn)程將被init進(jìn)程(進(jìn)程號為1)所收養(yǎng),并由init進(jìn)程對它們完成狀態(tài)收集工作。

孤兒進(jìn)程是沒有父進(jìn)程的進(jìn)程,孤兒進(jìn)程這個重任就落到了init進(jìn)程身上,init進(jìn)程就好像是一個民政局,專門負(fù)責(zé)處理孤兒進(jìn)程的善后工作。每當(dāng)出現(xiàn)一個孤兒進(jìn)程的時候,內(nèi)核就把孤 兒進(jìn)程的父進(jìn)程設(shè)置為init,而init進(jìn)程會循環(huán)地wait()它的已經(jīng)退出的子進(jìn)程。這樣,當(dāng)一個孤兒進(jìn)程凄涼地結(jié)束了其生命周期的時候,init進(jìn)程就會代表黨和政府出面處理它的一切善后工作。因此孤兒進(jìn)程并不會有什么危害。

守護(hù)進(jìn)程

守護(hù)進(jìn)程是由主進(jìn)程創(chuàng)建的子進(jìn)程:

  • 守護(hù)進(jìn)程會在主進(jìn)程代碼結(jié)束后就終止。
  • 守護(hù)進(jìn)程內(nèi)無法再開啟子進(jìn)程,否則拋出異常:AssertionError: daemonic processes are not allowed to have children
  • 進(jìn)程之間是相互獨立的,主進(jìn)程代碼運行結(jié)束,守護(hù)進(jìn)程隨即終止。

示例:

# coding=utf-8
import os
from multiprocessing import Process  # 進(jìn)程模塊
import time

def task(n):
    print('%s is running' %os.getpid())
    time.sleep(2)
    print('%s is done' %os.getpid())

if __name__ == '__main__':
    p = Process(target=task,args=('ggg',))
    p.daemon = True                     # 開啟守護(hù)進(jìn)程,必須在start之前設(shè)置
    p.start()
    print("主進(jìn)程")

輸出結(jié)果:

主進(jìn)程

說明: 由于開啟了守護(hù)進(jìn)程,當(dāng)主進(jìn)程運行結(jié)束時,子進(jìn)程(守護(hù)進(jìn)程)也會終止運行,不管守護(hù)進(jìn)程是否運行完畢。

守護(hù)進(jìn)程的使用場景: 當(dāng)子進(jìn)程執(zhí)行的任務(wù),在父進(jìn)程代碼運行完畢后就沒有運行的必要了,那么該子進(jìn)程就應(yīng)該設(shè)置為守護(hù)進(jìn)程。

守護(hù)進(jìn)程與普通子進(jìn)程測試:

# coding=utf-8
import os
from multiprocessing import Process  # 進(jìn)程模塊
import time

def foo():
    print('這是守護(hù)進(jìn)程')
    time.sleep(1)
    print('守護(hù)進(jìn)程結(jié)束')

def bar():
    print('這是普通子進(jìn)程')
    time.sleep(2)
    print('bar 運行結(jié)束')

if __name__ == '__main__':

    p1=Process(target=foo)
    p2=Process(target=bar)
    p1.daemon=True
    p1.start()
    p2.start()
    print("主進(jìn)程到此結(jié)束!")

運行結(jié)果:

主進(jìn)程到此結(jié)束!
這是普通子進(jìn)程
bar 運行結(jié)束
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容