1. 程式人生 > >Learning-Python【33】:並發編程之多進程

Learning-Python【33】:並發編程之多進程

app clas 我們 min 超時 inside pan auth strong

一、multiprocessing 模塊介紹

Python 中的多線程無法利用多核優勢,如果想要充分地使用多核 CPU 的資源(os.cpu_count()查看),在 Python 中大部分情況需要使用多進程。Python提供了 multiprocessing。

multiprocessing 模塊用來開啟子進程,並在子進程中執行我們定制的任務(比如函數),該模塊與多線程模塊 threading 的編程接口類似。

multiprocessing 模塊的功能眾多:支持子進程、通信和共享數據、執行不同形式的同步,提供了 Process、Queue、Pipe、Lock 等組件。

需要再次強調的一點是:與線程不同,進程沒有任何共享狀態,進程修改的數據,改動僅限於該進程內。

二、Process 類的介紹

1、創建進程的類

Process([group [, target [, name [, args [, kwargs]]]]]),由該類實例化得到的對象,可用來開啟一個子進程
 
強調:
1. 需要使用關鍵字的方式來指定參數
2. args 指定的為傳給 target 函數的位置參數,是一個元組形式,必須有逗號

2、參數介紹

group參數未使用,值始終為None

target表示調用對象,即子進程要執行的任務

args表示調用對象的位置參數元組,args=(1,2,‘qiu‘,)

kwargs表示調用對象的字典,kwargs={‘name‘:‘qiu‘,‘age‘:18
} name為子進程的名稱

3、方法介紹

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 開啟的進程

4、屬性介紹

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__‘: 下

Since Windows has no fork, the multiprocessing module starts a new Python
process and imports the calling module.
    If Process() gets called upon import, then this sets off an infinite succession of
new processes (or until your machine runs out of resources).
    This is the reason for hiding calls to Process() inside
 
    if __name__ == "__main__"
    since statements inside this if-statement will not get called upon import.
    由於Windows沒有fork,多處理模塊啟動一個新的Python進程並導入調用模塊。
    如果在導入時調用Process(),那麽這將啟動無限繼承的新進程(或直到機器耗盡資源)。
這是隱藏對Process()內部調用的原,使用if __name__ == “__main __”,這個if語句
中的語句將不會在導入時被調用。

創建並開啟子進程的兩種方式

技術分享圖片
from multiprocessing import Process
import time

def task(name):
    print("%s is running" %name)
    time.sleep(3)
    print("%s is done" %name)

if __name__ == __main__:
    p = Process(target=task, args=("qiu",))
    # p = Process(target=task, kwargs={"name": "qiu"})

    # p.start()只是向操作系統發送了一個開啟子進程的信號, 操作系統才能開啟子進程,
    # 涉及到申請內存空間, 要將父進程的數據拷貝到子進程, 要將CPU調到子進程裏運行子進程的代碼
    # 才會有 is running的顯示, 這都是一系列的硬件操作
    # 所以print("主")這行代碼運行速度要快一些
    p.start()
    print("")
方式一 技術分享圖片
from multiprocessing import Process
import time

class MyProcess(Process):

    def __init__(self, name):
        super().__init__()
        self.name = name

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

if __name__ == __main__:
    p = MyProcess("qiu")
    p.start()
    print("")
方式二

四、join方法

在主進程運行過程中如果想要並發的執行其他任務,我們可以開啟子進程,此時主進程的任務和子進程的任務分為兩種情況:

  一種情況是:在主進程的任務與子進程的任務彼此獨立的情況下,主進程的任務先執行完畢後,主進程還需要等待子進程執行完畢,然後統一回收資源

  還有一種情況是:如果主進程的任務在執行到某一個階段時,需要等待子進程執行完畢後才能繼續執行,就需要一種機制能夠讓主進程監測子進程是否運行完畢,在子進程執行完畢後才繼續執行,否則一直在原地阻塞,這就是 join 方法的作用。

技術分享圖片
from multiprocessing import Process
import time

def task(name, n):
    print("%s is running" %name)
    time.sleep(n)
    print("%s is done" %name)

if __name__ == __main__:
    p1 = Process(target=task, args=("Process 1", 1))
    p2 = Process(target=task, args=("Process 2", 2))
    p3 = Process(target=task, args=("Process 3", 3))

    start = time.time()
    p1.start()
    p2.start()
    p3.start()

    p1.join()
    p2.join()
    p3.join()
    print("主進程", time.time() - start)
join

  人會有疑問,既然 join 是等待進程結束,那麽我像下面 join 下去,進程不就變成串行了的嗎?

  當然不是了,必須明確 join 是讓誰等:進程只要 start 就會在開始運行了,所以 p1 到 p3.start() 時,系統中已經有三個並發的進程了,而 p1.join() 是在等 p1 結束,p1 只要不結束主線程就會一直卡在原地,這也是問題的關鍵。join 是讓主線程等,而 p1-p3 仍然是並發執行的,p1.join() 的時候,其余 p2,p3 仍然在運行,等 p1.join() 結束,可能 p2,p3 早已經結束了,這樣 p2.join(),p3.join() 直接通過檢測,無需等待。所以 3 個 join 花費的總時間仍然是耗費時間最長的那個進程運行的時間,所以這裏即便交換 join 的順序,執行的時間仍然是 3 秒多一點,多出來的那零點幾秒是開啟進程以及進程切換的時間。

技術分享圖片
from multiprocessing import Process
import time

def task(name, n):
    print("%s is running" %name)
    time.sleep(n)
    print("%s is done" %name)

if __name__ == __main__:
    p1 = Process(target=task, args=("Process 1", 1))
    p2 = Process(target=task, args=("Process 2", 2))
    p3 = Process(target=task, args=("Process 3", 3))

    start = time.time()
    p1.start()
    p2.start()
    p3.start()

    p3.join()
    p1.join()
    p2.join()

    print("主進程", time.time() - start)
交換join的順序

join 是讓主進程在原地等待,等待子進程運行完畢,不會影響子進程的執行

上面的代碼可以使用 for 循環簡寫

技術分享圖片
from multiprocessing import Process
import time

def task(name, n):
    print("%s is running" %name)
    time.sleep(n)
    print("%s is done" %name)

if __name__ == __main__:

    start = time.time()
    p_l = []
    for i in range(1, 4):
        p = Process(target=task, args=("Process %s" %i, i))
        p_l.append(p)
        p.start()

    for p in p_l:
        p.join()

    print("主進程", time.time() - start)
使用for循環簡寫

進程間的內存空間互相隔離

技術分享圖片
from multiprocessing import Process

n = 100

def task():
    global n
    n = 0

if __name__ == __main__:
    p = Process(target=task)
    p.start()
    p.join()
    print("主進程內的:", n)
View Code

僵屍進程與孤兒進程

  僵屍進程:一個進程使用 fork 創建子進程,如果子進程退出,而父進程並沒有調用 wait 或 waitpid 獲取子進程的狀態信息,那麽子進程的進程描述符仍然保存在系統中。這種進程稱之為僵死進程

  我們知道在 Unix/Linux 中,正常情況下子進程是通過父進程創建的,子進程在創建新的進程。子進程的結束和父進程的運行是一個異步過程,即父進程永遠無法預測子進程到底什麽時候結束,如果子進程一結束就立刻回收其全部資源,那麽在父進程內將無法獲取子進程的狀態信息。因此,Unix 提供了一種機制可以保證父進程可以在任意時刻獲取子進程結束時的狀態信息:

  1、在每個進程退出的時候,內核釋放該進程所有的資源,包括打開的文件,占用的內存等。但是仍然為其保留一定的信息(包括進程號、退出狀態、運行時間等)

  2、直到父進程通過 wait/waitpid 來取時才釋放。但這樣就導致了問題,如果進程不調用 wait/waitpid 的話,那麽保留的那段信息就不會釋放,其進程號就會一直被占用,但是系統所能使用的進程號是有限的,如果大量的產生僵死進程,將因為沒有可用的進程號而導致系統不能產生新的進程。此即為僵屍進程的危害,應當避免。

  任何一個子進程(init除外)在 exit() 之後,並非馬上就消失掉,而是留下一個稱為僵屍進程(Zombie)的數據結構,等待父進程處理。這是每個子進程在結束時都要經過的階段。如果子進程在 exit() 之後,父進程沒有來得及處理,這時用 ps 命令就能看到子進程的狀態是 “Z” 。如果父進程能及時 處理,可能用 ps 命令就來不及看到子進程的僵屍狀態,但這並不等於子進程不經過僵屍狀態。 如果父進程在子進程結束之前退出,則子進程將由 init 接管。init 將會以父進程的身份對僵屍狀態的子進程進行處理。

  孤兒進程:一個父進程退出,而它的一個或多個子進程還在運行,那麽那些子進程將成為孤兒進程。孤兒進程將被 init 進程(進程號為 1)所收養,並由 init 進程對它們完成狀態收集工作。

  孤兒進程是沒有父進程的進程,孤兒進程這個重任就落到了 init 進程身上,init 進程就好像是一個民政局,專門負責處理孤兒進程的善後工作。每當出現一個孤兒進程的時候,內核就把孤 兒進程的父進程設置為 init,而 init 進程會循環地 wait() 它的已經退出的子進程。這樣,當一個孤兒進程淒涼地結束了其生命周期的時候,init 進程就會代表黨和政府出面處理它的一切善後工作。因此孤兒進程並不會有什麽危害。

Learning-Python【33】:並發編程之多進程