Python之路(第三十七篇)併發程式設計:程序、multiprocess模組、建立程序方式、join()、守護程序
之前已經瞭解了很多程序相關的理論知識,瞭解程序是什麼應該不再困難了,執行中的程式就是一個程序。所有的程序都是通過它的父程序來建立的。因此,執行起來的python程式也是一個程序,那麼也可以在程式中再建立程序。多個程序可以實現併發效果,也就是說,當程式中存在多個程序的時候,在某些時候,就會讓程式的執行速度變快。在python中實現多程序需要藉助python中強大的模組。
二、multiprocess模組
python中的多執行緒無法利用多核優勢,如果想要充分地使用多核CPU的資源(os.cpu_count()檢視),在python中大部分情況需要使用多程序。Python提供了multiprocessing。 multiprocessing模組用來開啟子程序,並在子程序中執行我們定製的任務(比如函式),該模組與多執行緒模組threading的程式設計介面類似。
multiprocessing模組的功能眾多:支援子程序、通訊和共享資料、執行不同形式的同步,提供了Process、Queue、Pipe、Lock等元件。
需要再次強調的一點是:與執行緒不同,程序沒有任何共享狀態,程序修改的資料,改動僅限於該程序內。
Process類的介紹
建立程序的類:
Process([group [, target [, name [, args [, kwargs]]]]]),由該類例項化得到的物件,表示一個子程序中的任務(尚未啟動) 強調: 1. 需要使用關鍵字的方式來指定引數 2. args指定的為傳給target函式的位置引數,是一個元組形式,必須有逗號
引數介紹:
group引數未使用,值始終為None target表示呼叫物件,即子程序要執行的任務 args表示呼叫物件的位置引數元組,args=(1,2,'egon',) kwargs表示呼叫物件的字典,kwargs={'name':'egon','age':18} name為子程序的名稱
方法介紹:
p.start():啟動程序,並呼叫該子程序中的p.run() p.run():程序啟動時執行的方法,正是它去呼叫target指定的函式,我們自定義類的類中一定要實現該方法 p.terminate():強制終止程序p,不會進行任何清理操作,如果p建立了子程序,該子程序就成了殭屍程序,使用該方法需要特別小心這種情況。如果p還儲存了一個鎖那麼也將不會被釋放,進而導致死鎖 p.is_alive():如果p仍然執行,返回True p.join([timeout]):主執行緒等待p終止(強調:是主執行緒處於等的狀態,而p是處於執行的狀態)。timeout是可選的超時時間,需要強調的是,p.join只能join住start開啟的程序,而不能join住run開啟的程序
屬性介紹:
p.daemon:預設值為False,如果設為True,代表p為後臺執行的守護程序,當p的父程序終止時,p也隨之終止,並且設定為True後,p不能建立自己的新程序,必須在p.start()之前設定 p.name:程序的名稱 p.pid:程序的pid p.exitcode:程序在執行時為None、如果為–N,表示被訊號N結束(瞭解即可) p.authkey:程序的身份驗證鍵,預設是由os.urandom()隨機生成的32字元的字串。這個鍵的用途是為涉及網路連線的底層程序間通訊提供安全性,這類連線只有在具有相同的身份驗證鍵時才能成功(瞭解即可)
Process類的使用
注意:在windows中Process()必須放到# if name == 'main':下
建立並開啟子程序的兩種方式
方式一
#方式一:直接用函式 import multiprocessing # from multiprocessing import Process 這種匯入模組的方式可以在下面程式碼中直接寫Process(target= ,args=) import time def hi(name): print("hello %s"%name) time.sleep(1) if __name__ == "__main__": p = multiprocessing.Process(target=hi,args=("nick",)) p.start() p.join() print("ending...")
方式二
#開啟程序的方式二,在類中啟動程序 import time import multiprocessing class Foo(multiprocessing.Process): #這裡繼承 multiprocessing.Process類 def __init__(self,name): super().__init__() self.name = name def run(self): print("hello %s" % self.name) time.sleep(3) if __name__ == "__main__": p = Foo("nick") p.start() #這裡執行start()會直接呼叫類的run()方法 # p.join() print("ending...")
程序直接的記憶體空間是隔離的
import multiprocessing n = 100 def work(): global n n = 0 print("子程序內的n", n) if __name__ == '__main__': p = multiprocessing.Process(target=work) p.start() print("主程序內的n", n)
輸出結果
主程序內的n 100 子程序內的n 0
分析:由於子程序和主程序是隔離的,所以即使在子程序裡有global關鍵字,主程序同一變數的值也沒變。
Process物件的join方法
在主程序執行過程中如果想併發地執行其他的任務,我們可以開啟子程序,此時主程序的任務與子程序的任務分兩種情況
情況一:在主程序的任務與子程序的任務彼此獨立的情況下,主程序的任務先執行完畢後,主程序還需要等待子程序執行完畢,然後統一回收資源。
情況二:如果主程序的任務在執行到某一個階段時,需要等待子程序執行完畢後才能繼續執行,就需要有一種機制能夠讓主程序檢測子程序是否執行完畢,在子程序執行完畢後才繼續執行,否則一直在原地阻塞,這就是join方法的作用。
例子
from multiprocessing import Process import time def func(args): print('-----',args) time.sleep(2) print("end---") if __name__ == '__main__': p = Process(target=func,args=(1,)) p.start() print('哈哈哈哈') p.join()#join()的作用是阻塞主程序,使得主程序等待子程序執行完才把自己結束 print("主程序執行完了")
例子2
from multiprocessing import Process import time import random def piao(name): print('%s is talking' %name) time.sleep(random.randint(1,3)) print('%s is talking end' %name) p1=Process(target=piao,args=('nick',)) p2=Process(target=piao,args=('jack',)) p3=Process(target=piao,args=('pony',)) p4=Process(target=piao,args=('charles',)) p1.start() p2.start() p3.start() p4.start() #疑問:既然join是等待程序結束,那麼像下面這樣寫,程序不就又變成序列的了嗎? #當然不是了,必須明確:p.join()是讓誰等? #很明顯p.join()是讓主執行緒等待p的結束,卡住的是主執行緒而絕非程序p, #詳細解析如下: #程序只要start就會在開始運行了,所以p1-p4.start()時,系統中已經有四個併發的程序了 #而p1.join()是在等p1結束,沒錯p1只要不結束主執行緒就會一直卡在原地,這也是問題的關鍵 #join是讓主執行緒等,而p1-p4仍然是併發執行的,p1.join的時候,其餘p2,p3,p4仍然在執行,等#p1.join結束,可能p2,p3,p4早已經結束了,這樣p2.join,p3.join.p4.join直接通過檢測,無需等待 # 所以4個join花費的總時間仍然是耗費時間最長的那個程序執行的時間 p1.join() p2.join() p3.join() p4.join() print('主執行緒') #上述啟動程序與join程序可以簡寫為 # p_l=[p1,p2,p3,p4] # # for p in p_l: # p.start() # # for p in p_l: # p.join()
Process物件的其他方法或屬性
程序物件的其他方法:terminate與is_alive、name與pid
例子
import multiprocessing import time def task(name, t): print("程序正在執行。。。%s--%s" % (name, time.asctime())) time.sleep(t) print("程序結束了%s--%s" % (name, time.asctime())) if __name__ == "__main__": p1 = multiprocessing.Process(target=task, args=("nick", 5), name="程序1") # 注意這裡的引數name不是函式的引數 p2 = multiprocessing.Process(target=task, args=("nicholas", 2), name="程序2") p_list = [] p_list.append(p1) p_list.append(p2) print("程序是否存活", p1.is_alive()) for p in p_list: p.start() print(p.name) # 程序.name輸出程序的名稱 print(p.name, p.pid) # 程序物件.pid也可以輸出子程序的ID號, print("程序是否存活", p1.is_alive()) # 判斷子程序是否存活 # p2.terminate()#不管程序是否執行完,立刻結束程序 for p in p_list: p.join() print("ending...")
通過os模組檢視pid和ppid
import multiprocessing import os class Foo(multiprocessing.Process): def __init__(self,name): super().__init__() self.name = name def run(self): print("hi %s,子程序號是%s"%(self.name,os.getpid()))#輸出當前的程序的pid if __name__ == "__main__": p = Foo("nick") p.start() print("主程序是%s"%os.getppid()) #這裡的主程序好就是執行這個py檔案的程式,這裡是pycharm, # 如果用命令終端執行py檔案則主程序是命令終端的號 #os.getppid()是輸出當前程序的父程序pid號 #os.getpid()是輸出當前的程序的pid
殭屍程序與孤兒程序(瞭解)
一:殭屍程序(有害) 殭屍程序:一個程序使用fork建立子程序,如果子程序退出,而父程序並沒有呼叫wait或waitpid獲取子程序的狀態資訊,那麼子程序的程序描述符仍然儲存在系統中。這種程序稱之為僵死程序。詳解如下 我們知道在unix/linux中,正常情況下子程序是通過父程序建立的,子程序在建立新的程序。子程序的結束和父程序的執行是一個非同步過程,即父程序永遠無法預測子程序到底什麼時候結束,如果子程序一結束就立刻回收其全部資源,那麼在父程序內將無法獲取子程序的狀態資訊。 因此,UNⅨ提供了一種機制可以保證父程序可以在任意時刻獲取子程序結束時的狀態資訊: 1、在每個程序退出的時候,核心釋放該程序所有的資源,包括開啟的檔案,佔用的記憶體等。但是仍然為其保留一定的資訊(包括程序號the process ID,退出狀態the termination status of the process,執行時間the amount of CPU time taken by the process等) 2、直到父程序通過wait / waitpid來取時才釋放. 但這樣就導致了問題,如果程序不呼叫wait / waitpid的話,那麼保留的那段資訊就不會釋放,其程序號就會一直被佔用,但是系統所能使用的程序號是有限的,如果大量的產生僵死程序,將因為沒有可用的程序號而導致系統不能產生新的程序. 此即為殭屍程序的危害,應當避免。 任何一個子程序(init除外)在exit()之後,並非馬上就消失掉,而是留下一個稱為殭屍程序(Zombie)的資料結構,等待父程序處理。這是每個子程序在結束時都要經過的階段。如果子程序在exit()之後,父程序沒有來得及處理,這時用ps命令就能看到子程序的狀態是“Z”。如果父程序能及時 處理,可能用ps命令就來不及看到子程序的殭屍狀態,但這並不等於子程序不經過殭屍狀態。 如果父程序在子程序結束之前退出,則子程序將由init接管。init將會以父程序的身份對殭屍狀態的子程序進行處理。 二:孤兒程序(無害) 孤兒程序:一個父程序退出,而它的一個或多個子程序還在執行,那麼那些子程序將成為孤兒程序。孤兒程序將被init程序(程序號為1)所收養,並由init程序對它們完成狀態收集工作。 孤兒程序是沒有父程序的程序,孤兒程序這個重任就落到了init程序身上,init程序就好像是一個民政局,專門負責處理孤兒程序的善後工作。每當出現一個孤兒程序的時候,核心就把孤 兒程序的父程序設定為init,而init程序會迴圈地wait()它的已經退出的子程序。這樣,當一個孤兒程序淒涼地結束了其生命週期的時候,init程序就會出面處理它的一切善後工作。因此孤兒程序並不會有什麼危害。 三:殭屍程序危害場景: 例如有個程序,它定期的產 生一個子程序,這個子程序需要做的事情很少,做完它該做的事情之後就退出了,因此這個子程序的生命週期很短,但是,父程序只管生成新的子程序,至於子程序 退出之後的事情,則一概不聞不問,這樣,系統執行上一段時間之後,系統中就會存在很多的僵死程序,倘若用ps命令檢視的話,就會看到很多狀態為Z的程序。 嚴格地來說,僵死程序並不是問題的根源,罪魁禍首是產生出大量僵死程序的那個父程序。因此,當我們尋求如何消滅系統中大量的僵死程序時,答案就是把產生大 量僵死程序的那個元凶槍斃掉(也就是通過kill傳送SIGTERM或者SIGKILL訊號啦)。槍斃了元凶程序之後,它產生的僵死程序就變成了孤兒進 程,這些孤兒程序會被init程序接管,init程序會wait()這些孤兒程序,釋放它們佔用的系統程序表中的資源,這樣,這些已經僵死的孤兒程序 就能瞑目而去了。
守護程序
會隨著主程序的結束而結束。
主程序建立守護程序
其一:守護程序會在主程序程式碼執行結束後就終止
其二:守護程序內無法再開啟子程序,否則丟擲異常:AssertionError: daemonic processes are not allowed to have children
注意:程序之間是互相獨立的,主程序程式碼執行結束,守護程序隨即終止,不管守護程序執行沒執行完。
例子1
from multiprocessing import Process import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': p1=Process(target=foo) p2=Process(target=bar) p1.daemon=True ##一定要在p1.start()前設定,設定p1為守護程序, # 禁止p1建立子程序,並且父程序程式碼執行結束,p1即終止執行 p1.start() p2.start() print("main-------") #只要終端打印出這一行內容,那麼守護程序p也就跟著結束掉了
例子2
# 主程序程式碼執行完畢,守護程序就會結束 from multiprocessing import Process import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': p1 = Process(target=foo) p2 = Process(target=bar) p1.daemon = True p1.start() p2.start() print("main-------") # 列印該行則主程序程式碼結束,則守護程序p1應該被終止, # 可能會有p1任務執行的列印資訊123,因為主程序列印main----時,p1也執行了,但是隨即被終止 #這時p1可能會被執行,也可能不會被執行
參考資料
[1]http://www.cnblogs.com/Anker/p/3271773.html