python3 進程(multiprocessing)從入門到提高--詳解

0.背景知識:

1.殭屍進程(有害)

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

2.孤兒進程(無害)

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

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

3.總結

嚴格地來說,僵死進程並不是問題的根源,罪魁禍首是產生出大量僵死進程的那個父進程。因此,當我們尋求如何消滅系統中大量的僵死進程時,答案就是把產生大 量僵死進程的那個元兇槍斃掉(也就是通過kill發送SIGTERM或者SIGKILL信號啦)。槍斃了元兇進程之後,它產生的僵死進程就變成了孤兒進 程,這些孤兒進程會被init進程接管,init進程會wait()這些孤兒進程,釋放它們佔用的系統進程表中的資源,這樣,這些已經僵死的孤兒進程 就能瞑目而去了。

1.開啓進程的兩種方式

1.簡單開啓

# 方式一:使用函數開啓進程
from multiprocessing import Process
import time


def task(x):
    print('%s is running' % x)
    time.sleep(1)
    print('%s is done' % x)


if __name__ == '__main__':
    # p1 = Process(target=task, args=('子進程',))    #實例化子進程
    p1 = Process(target=task, kwargs={'x': '子進程'})
    p1.start()  # 向操作系統申請資源(內存空間,子進程pid號),然後開始執行task任務,本動作不影響主進程,主進程則會繼續執行。
    print('這是主進程...')

使用函數開啓進程

2.類方式開啓

from multiprocessing import Process
import time

class MyProcess(Process):

    def __init__(self,x):
        super(MyProcess,self).__init__()
        self.x = x

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

if __name__ == '__main__':
    p1 = MyProcess('子進程')
    p1.start()
    print('這是主進程....')

使用類的方式開啓進程

2.進程間是物理隔離的,不共享全局變量


from multiprocessing import Process
import time

x = 100


def task():
    global x
    x += 1
    print('子進程中執行x所得的值爲------%s' % x)
    time.sleep(1)


if __name__ == '__main__':
    p1 = Process(target=task)
    p1.start()
    p1.join()
    print('父進程中執行x所得的值爲------%s' % x)

# 子進程中執行x所得的值爲------101
# 父進程中執行x所得的值爲------100

子進程是父進程的複製品,在內存中會把父進程的代碼及內存分配情況拷貝一份生成子進程的運行空間,這樣子進程與父進程的所有代碼都一樣,兩個進程之間的運行時獨立的,互不影響

3.進程中使用join()函數

  • 如果子進程不使用join,那麼主進程就不會等待子進程,單獨運行直到結束。
  • 子進程調用join時,主進程會被阻塞。當子進程結束後,主進程才能繼續執行。
  • join 函數的作用 功能是爲了實現進程同步的(可以理解爲子進程與主進程是同步的,一個完成了,才能完成另一個的意思)
  • 而它具體 是阻塞當前進程也就是主進程直到調用join函數的進程執行完成後繼續執行當前進程。

1.子進程中不使用join()

import os
from multiprocessing import Process


def run_proc(name):
    print("Child process %s (%s)" % (name, os.getpid()))


if __name__ == "__main__":
    print("Parent process %s" % (os.getpid()))
    for i in range(5):
        p = Process(target=run_proc, args=(str(i),))
        p.start()
    print("Finished")

Parent process 2460
Child process 0 (2461)
Child process 1 (2462)
Finished
Child process 2 (2463)
Child process 3 (2464)
Child process 4 (2465)

通過如上的結果發現:

  • 主進程都結束了,但是子進程還沒有結束,直到子進程運行完成。
  • 沒有join,主進程不會等待任何子進程,獨立運行,不受任何其他子進程影響。

2.子進程中使用join()

import os
import time
from multiprocessing import Process


def pro_func01(name):
    print("Child process %s (%s)" % (name, os.getpid()))


def pro_func02(name):
    for i in range(5):
        time.sleep(1)
        print("一共睡眠了 %s 秒種" % i)
    print("Child process %s (%s)" % (name, os.getpid()))


if __name__ == "__main__":
    print("Parent process %s" % (os.getpid()))
    p01 = Process(target=pro_func01, args=(str(1),))
    p02 = Process(target=pro_func02, args=(str(2),))
    p01.start()
    p02.start()
    p02.join()
    print("Finished")

Parent process 2608
Child process 1 (2609)
一共睡眠了 0 秒種
一共睡眠了 1 秒種
一共睡眠了 2 秒種
一共睡眠了 3 秒種
一共睡眠了 4 秒種
Child process 2 (2610)
Finished

通過以上發現,因爲p2進程使用了join函數,所以主進程等待這個子程序運行結束之後纔去執行主程序的最後一個打印工作。

3.多個子進程在for循環中錯誤使用join函數

import os
from multiprocessing import Process
from time import sleep
import time


def run_proc(name):
    sleep(1)
    print("Child process %s (%s)" % (name, os.getpid()))


if __name__ == "__main__":
    start_time = time.time()
    print("Parent process %s" % (os.getpid()))
    for i in range(5):
        p = Process(target=run_proc, args=(str(i),))
        p.start()
        p.join()
    end_time = time.time()
    print("Finished", end_time - start_time)

Parent process 2692
Child process 0 (2693)
Child process 1 (2695)
Child process 2 (2696)
Child process 3 (2701)
Child process 4 (2702)
Finished 5.041041851043701

以上是結果可以看到:

  • 在主進程中首先進入for循環啓動第一個子進程然後由於調用join函數會阻塞主進程,直到第一個子進程執行完成後開始取消阻塞。
  • 繼續執行主進程,開始開啓第二個子進程,然後由於第二個子進程也調用了join,再阻塞主進程一直到循環結束。
  • 最終循環下來,直到所有子進程,都一個一個執行完之後,纔開始執行主進程。
  • 這種方式可以實現效果,但是效率太低,既然是一個一個進程執行的,和我們使用一個進程for循環是一樣的效果,沒有體現多核CPU併發異步執行任務的效果。

4.多個子進程在for循環中使用join函數(改進)

import os
from multiprocessing import Process
from time import sleep
import time


def run_proc(name):
    sleep(1)
    print("Child process %s (%s)" % (name, os.getpid()))


if __name__ == "__main__":
    start_time = time.time()
    print("Parent process %s" % (os.getpid()))
    p_list = list()
    for i in range(5):
        p = Process(target=run_proc, args=(str(i),))
        p_list.append(p)
        p.start()

    for m in p_list:
        m.join()
    end_time = time.time()
    print("Finished", end_time - start_time)



Parent process 2745
Child process 0 (2746)
Child process 1 (2747)
Child process 2 (2748)
Child process 3 (2749)
Child process 4 (2750)
Finished 1.013887882232666

通過如上的改進,我們先啓動所有子進程,所有任務都啓動完成之後,再使用join()這樣可以實現多線程的異步執行(併發的概念)。效率也提高很多

4.daemon守護進程的作用

1.不設置守護進程效果

import time
from multiprocessing import Process


def func():
    print("子進程開始.")
    time.sleep(2)
    print("子進程結束.")


if __name__ == '__main__':
    p = Process(target=func)
    p.start()
    print("主進程結束.")
主進程結束.
子進程開始.
子進程結束.

不設置守護進程的時候,雖然主進程沒有等待子進程,直接先結束了。但是,前面我們提到過init進程的作用,它會接收孤兒進程,讓孤兒進程繼續執行下去。

2.設置守護進程效果

import time
from multiprocessing import Process


def func():
    print("子進程開始.")
    time.sleep(2)
    print("子進程結束.")


if __name__ == '__main__':
    p = Process(target=func)
    p.daemon = True
    p.start()
    print("主進程結束.")
主進程結束.

通過如上的結果:

  • 設置了守護進程,主進程還是沒有等待子進程,直接結束了。但是,主進程結束了,子進程也跟着結束了。也就說明,設置守護進程之後,init進程,就不會去接收這個孤兒進程了。這個是進程是否被設置爲守護進程的最大區別。
  • daemon一定要在p.start()前設置,設置p爲守護進程,禁止p創建子進程,並且父進程代碼執行結束,p即終止運行

主進程創建守護進程
   其一:守護進程會在主進程代碼執行結束後就終止
   其二:守護進程內無法再開啓子進程,否則拋出異常:AssertionError: daemonic processes are not allowed to have children

5.進程間通訊隊列(Queue,JoinableQueue)

1.使用Queue通訊

多進程裏面使用的隊列(Queue)是multiprocessing模塊裏面的Queue,和queue.Queue的隊列模塊不一樣

import time
from multiprocessing import Process
from multiprocessing import Queue


def producer(food, q, name):
    for i in range(3):
        res = '%s%s' % (food, i)
        time.sleep(2)
        q.put(res)
        print('%s 生產了%s' % (name, res))
    q.put(name)


def consumer(q, name):
    while True:
        res = q.get()
        if res in ("worker_01", "worker_02", "worker_03"):
            print(res, "-------???-------")
            break
        time.sleep(3)
        print('----------%s消費了%s' % (name, res))


if __name__ == '__main__':
    q = Queue()
    p1 = Process(target=producer, args=('包子', q, 'worker_01'))
    p2 = Process(target=producer, args=('水餃', q, 'worker_02'))
    p3 = Process(target=producer, args=('饅頭', q, 'worker_03'))

    c1 = Process(target=consumer, args=(q, 'Consumer_01'))
    c2 = Process(target=consumer, args=(q, 'Consumer_02'))

    p1.start()
    p2.start()
    p3.start()

    c1.start()
    c2.start()

    p1.join()
    p2.join()
    p3.join()
    print('主...')

worker_01 生產了包子0
worker_02 生產了水餃0
worker_03 生產了饅頭0
worker_03 生產了饅頭1
worker_02 生產了水餃1
worker_01 生產了包子1
----------Consumer_01消費了包子0
----------Consumer_02消費了水餃0
worker_03 生產了饅頭2
worker_01 生產了包子2
worker_02 生產了水餃2...
----------Consumer_01消費了饅頭0
----------Consumer_02消費了包子1
----------Consumer_01消費了饅頭1
----------Consumer_02消費了水餃1
----------Consumer_01消費了水餃2
----------Consumer_02消費了包子2
worker_03 -------???-------
----------Consumer_01消費了饅頭2
worker_02 -------???-------

通過如上的結果:

  • 我們通過一種將隊列裏面input()固定的參數的形式,如果遇到這個參數,就break就可以停止從隊列裏面get()信息,也避免因隊列爲空的時候,再獲取信息的報錯。
  • 但這種方式容易有個弊端,通過如上的結果是看不出的(生產9個產品,消費了9個)。但如果如上的代碼只有一個消費者,得到的結果如下:
worker_01 生產了包子0
worker_03 生產了饅頭0
worker_02 生產了水餃0
worker_01 生產了包子1
worker_03 生產了饅頭1
worker_02 生產了水餃1
----------Consumer_01消費了包子0
worker_01 生產了包子2
worker_03 生產了饅頭2
worker_02 生產了水餃2...
----------Consumer_01消費了水餃0
----------Consumer_01消費了饅頭0
----------Consumer_01消費了包子1
----------Consumer_01消費了饅頭1
----------Consumer_01消費了水餃1
----------Consumer_01消費了水餃2
----------Consumer_01消費了包子2
worker_02 -------???-------

如上結果,生產了9個,但是隻消費了8個就結束了,這個不是我們想要的結果,
原因是我們將產品input到隊列的時候,產品和特殊字符的順序可能不一致,可能有多種多樣的結果,例如如下幾種情況:

order_product = ["包子0", "水餃0", '饅頭0', '包子1', '水餃1', '饅頭1', '包子2', 'worker_01', '水餃2', 'worker_02', '饅頭2', 'worker_03']

如上這種情況,如果只有2個消費者,那麼只能消費8個產品,如果一個消費者可以消費7個產品,因爲遇到特殊字符(worker_01或worker_02等就直接結束消費了),所以使用Queue來進程間的通訊,只適合非常簡答的場景。

2.使用JoinableQueue隊列

(1)消費者不需要判斷從隊列裏拿到某個特定字符(None或其他),再退出執行消費者函數了。
(2)消費者每次從隊列裏面q.get()一個數據,處理過後就使用隊列.task_done()
(3)生產者for循環生產完所有產品,需要q.join()阻塞一下,對這個隊列進行阻塞。意思是隻有當隊列裏面的所有內容都取完了,纔會調用主進程,繼續執行下去
(4)啓動一個生產者,啓動一個消費者,並且這個消費者做成守護進程,然後生產者需要p.join()阻塞一下。


import time
import random
from multiprocessing import JoinableQueue
from multiprocessing import Process


def producer(food, q, name):
    for i in range(3):
        res = '%s%s' % (food, i)
        time.sleep(random.randint(1, 3))
        q.put(res)
        print('%s 生產了%s' % (name, res))


def consumer(q, name):
    while True:
        res = q.get()
        time.sleep(random.randint(2, 4))
        print('----------%s消費了%s' % (name, res))
        q.task_done()


if __name__ == '__main__':
    q = JoinableQueue()
    p1 = Process(target=producer, args=('包子', q, 'worker_01'))
    p2 = Process(target=producer, args=('水餃', q, 'worker_02'))
    p3 = Process(target=producer, args=('饅頭', q, 'worker_03'))

    c1 = Process(target=consumer, args=(q, 'Consumer_01'))
    c2 = Process(target=consumer, args=(q, 'Consumer_02'))

    p1.start()
    p2.start()
    p3.start()
    c1.daemon = True
    c2.daemon = True
    c1.start()
    c2.start()

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

    q.join()
    print('主...')


worker_02 生產了水餃0
worker_01 生產了包子0
worker_03 生產了饅頭0
----------Consumer_01消費了水餃0
worker_01 生產了包子1
worker_02 生產了水餃1
worker_03 生產了饅頭1
----------Consumer_02消費了包子0
worker_02 生產了水餃2
worker_01 生產了包子2
----------Consumer_02消費了包子1
----------Consumer_01消費了饅頭0
worker_03 生產了饅頭2
----------Consumer_02消費了水餃1
----------Consumer_01消費了饅頭1
----------Consumer_02消費了水餃2
----------Consumer_02消費了饅頭2
----------Consumer_01消費了包子2...

通過如上發現:
1.q.join()阻塞了隊列,只有隊列的信息都取完之後,才能調用主進程結束
2.q.task_done()的作用是,每次從隊列裏面取出內容之後,使用者使用此方法發出信號,表示q.get()的返回項目已經被處理。如果調用此方法的次數大於從隊列中刪除項目的數量,將引發ValueError異常。(個人理解,這個函數的作用就是告訴主進程,我又取出來一個函數,直到主進程發現隊列裏面沒有了任何信息,此時主進程纔會執行,否者主進程就一直等待子進程的執行)
3.如果如上代碼不使用q.task_done(),那最終生產和消費都正常,但是主進程一直無法結束,一直在子進程的死循環中運行。

6.進程池

1.使用兩種不同的方式啓動進程池

# for循環方式啓動
from multiprocessing import Process
import time


def task(i):
    i += 1


if __name__ == '__main__':
    start = time.time()

    p_l = []

    for i in range(200):
        p = Process(target=task, args=(i,))
        p_l.append(p)
        p.start()

    for j in p_l:
        j.join()

    print(time.time() - start)

# 計算的時間爲:0.20714521408081055

from multiprocessing import Pool
import time


def task(i):
    i += 1


if __name__ == '__main__':
    '''
    分配進程個數時,推薦建議使用cpu核數+1
    '''

    start = time.time()
    p = Pool(5)  # 相當於實例化了 5 個進程
    p.map(task, range(200))  # 相當於 p.start()  ,天生異步,每次執行5個進程

    p.close()  # 爲了防止繼續往裏面提交任務,保護進程池,關閉掉進程池所接收的任務通道
    p.join()  # 等待子進程執行完畢

    print(time.time() - start)  # 計算開啓進程所消耗的總時間

# 計算最終的時間爲: 0.11593031

最終發現,使用第二種方式,直接創建進程池,然後把任務需要執行的數據,放在一個列表裏面傳遞給函數即可。

2.進程池使用apply啓動進程(同步)

from multiprocessing import Pool
import time
import random


def task(i):
    i += 1
    print(i)
    time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    p = Pool(5)
    for i in range(20):
        p.apply(task, args=(i,))  # apply天生同步


雖然啓動的進程池,但是所有進程還是一個一個執行的,和單進程沒啥區別,不推薦使用。

3.進程池使用apply_async(異步,但錯誤使用)

from multiprocessing import Pool
import time
import random


def task(i):
    i += 1
    print(i)
    time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    p = Pool(5)
    for i in range(20):
        res = p.apply_async(task, args=(i,))  # apply_async天生異步
        res.get()  # 如果在for循環中,使用res.get(),則整個程序又變成了同步的狀態

4.進程池使用apply_async(異步,優化之後的使用)


from multiprocessing import Pool
import time
import random


def task(i):
    i += 1
    print(i)
    time.sleep(random.randint(1, 2))


if __name__ == '__main__':
    p = Pool(5)
    res_li = []

    for i in range(20):
        res = p.apply_async(task, args=(i,))  # apply_async天生異步
        res_li.append(res)  # 將所有 apply_async返回的結果 都添加到列表中再get(),則程序又恢復異步

    for res in res_li:
        res.get()

每次啓動5個進程,去執行task任務,分4個批次執行完成。基本實現了進程的併發

文章來源:
https://www.cnblogs.com/lich1x/p/10235610.html
https://blog.csdn.net/weixin_43751285/article/details/92837030
https://www.cnblogs.com/gengyi/p/8564052.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章