Python使用multiprocessing庫實現多進程

在Python中執行數據處理任務時,可能執行非常緩慢,這時可以將一個進程任務拆分爲多個子進程,利用CPU的多個核心併發執行多個進程的方式來加速程序的執行。python中用於處理多進程相關的包爲multiprocessing,通過Process、Queue、Pipe、Lock等類實現子進程、通信和共享數據、進程同步等功能。

1、進程的創建和執行

有兩種創建子進程的方式,第一種是直接通過Process()創建子進程對象,第二種是通過繼承multiprocessing.Process類的方式,先創建子進程類然後再實例化子進程對象。

Process(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None),其中target爲子進程要執行的函數名稱,args以元組的方式爲函數傳入參數。生成的對象通過start()開始執行進程,is_alive()返回是否存活,通過pidname屬性獲取進程id和名字。join()會阻塞調用進程指導本進程執行結束,terminate()終止進程。

如下所示,創建多個子進程並啓動,注意不可以通過for循環來啓動子進程,那樣子進程會依次執行,而不是併發。程序運行後等待兩秒後一同輸出“第1/2/3個子進程”

import time
from multiprocessing import Process

def process_func(num):
    time.sleep(2)
    print('第', num, '個進程')


if __name__ == '__main__':
    process_list = []
    # 循環創建多個子進程
    for i in range(3):
        p = Process(target=process_func, args=(i+1,))
        process_list.append(p)

    # 依次啓動進程
    process_list[0].start()
    process_list[1].start()
    process_list[2].start()

如下通過類繼承的方式創建子進程類,在__init__()方法中接收參數,並重寫run()方法來定義子進程所需要執行的操作

class ClockProcess(Process):
    def __init__(self, num):	# 接收參數
        Process.__init__(self)
        self.num = num
	
    def run(self):				# 定義子進程執行的操作
        time.sleep(2)
        print('第', self.num, '個進程')


if __name__ == '__main__':
    p4 = ClockProcess(4)
    p4.start()

守護進程:如果子進程的daemon屬性設置爲True,則其在主進程結束後會自動結束子進程,

def process_func(num):
    time.sleep(2)
    print('第', num, '個進程')


if __name__ == '__main__':
    p = Process(target=process_func, args=(5,))
    # p.daemon = True								# 在start()之前開啓daemon屬性
    p.start()
    print('主進程結束')

上面的程序運行結果如下,主進程在開啓子進程後繼續運行,輸出“主進程結束”,子進程繼續執行,先休眠兩秒,然後輸出“第 5 個進程”

主進程結束
第 5 個進程

但是在設置p.daemon = True,運行只輸出“主進程結束”,子進程不會輸出,說明主進程結束後子進程也隨之結束

進程阻塞:通過join()方法可以阻塞調用進程,直到子進程運行結束,例如下面程序在使用p.join()阻塞主進程,主進程會等待子進程運行結束輸出“第 5 個進程”,再繼續執行輸出“主進程結束”,

if __name__ == '__main__':
    p = Process(target=process_func, args=(5,))
    p.daemon = True
    p.start()
    p.join()					# 阻塞進程
    print('主進程結束')
'''
第 5 個進程
主進程結束
'''

2、互斥與同步

Lock鎖

multiprocessing提供了Lock類用於實現進程鎖機制,可以使用with的方式來進行鎖管理,或者手動使用lock.acquire()、lock.release()來獲取或者釋放鎖。如下所示連個進程分別對文件進行寫入,通過鎖管理,實現了一個進程寫完之後,另一個進程再寫入

import multiprocessing


def prcess1(lock, f):
    with lock:  # 使用with進行鎖管理
        fs = open(f, 'a+')
        n = 3
        while n > 1:
            fs.write("進程1寫入文件\n")
            n -= 1
        fs.close()


def process2(lock, f):
    lock.acquire()  # 獲取鎖
    try:
        fs = open(f, 'a+')
        n = 3
        while n > 1:
            fs.write("進程2寫入文件\n")
            n -= 1
        fs.close()
    finally:
        lock.release()  # 釋放鎖


if __name__ == "__main__":
    lock = multiprocessing.Lock()
    f = "file.txt"
    w = multiprocessing.Process(target=prcess1, args=(lock, f))
    nw = multiprocessing.Process(target=process2, args=(lock, f))
    w.start()
    nw.start()
'''
file.txt
進程2寫入文件
進程2寫入文件
進程1寫入文件
進程1寫入文件
'''

信號量Semaphore

爲了互斥對多個同類資源的訪問,引入了信號量機制,它和鎖不同之處在於Lock每次只允許一個進程獲得鎖,Semaphore可以指定同時有多個進程獲得資源

import multiprocessing
import time


def worker(s):
    s.acquire()
    print(multiprocessing.current_process().name + "acquire")
    time.sleep(2)
    print(multiprocessing.current_process().name + "release\n")
    s.release()


if __name__ == "__main__":
    s = multiprocessing.Semaphore(2)  # 最大資源數爲2
    for i in range(5):
        p = multiprocessing.Process(target=worker, args=(s,))
        p.start()
'''
Process-2acquire
Process-3acquire
Process-3release

Process-2release

Process-1acquire
Process-4acquire
Process-1release

Process-4release

Process-5acquire
Process-5release
'''

Event同步

multiprocessing提供了Event用於實現進程之間的同步,例如下面的進程2開始執行後,通過wait()等待event,進程1通過set()觸發event後,進程2再繼續執行

def process1(e):
    time.sleep(2)
    e.set()  # 通過set()觸發event
    print('子進程1觸發event')


def process2(e):
    print('進程2等待中。。。')
    e.wait()  # 等待event被觸發
    print('子進程2繼續執行')


if __name__ == "__main__":
    e = multiprocessing.Event()
    p1 = multiprocessing.Process(target=process1, args=(e,))
    p2 = multiprocessing.Process(target=process2, args=(e,))
    p1.start()
    p2.start()
    print('主進程結束')
'''
主進程結束
進程2等待中。。。
子進程1觸發event
子進程2繼續執行
'''

3、進程通信

隊列Queue

multiprocessing提供了Queue類來實現進程之間數據的傳遞。
通過put()將數據放入隊列,當隊列滿時,如果屬性block=False會立即拋出異常queues.Full;若block爲True(默認值),會等待timeout的時間再嘗試放入,失敗後拋出異常。
通過get()從隊列取出數據,同樣地在隊列爲空時,若block=False會拋出異常queues.Empty;block=True會等待timeout的時間取出

如下所示寫進程write_process每隔兩秒寫入隊列,讀進程read_process接收數據

def write_process(q):
    try:
        for i in range(3):
            time.sleep(2)
            q.put(1, block=True, timeout=2)     # 將數據放入隊列
    except multiprocessing.queues.Full:
        print('隊列已滿')


def read_process(q):
    try:
        for i in range(3):
            msg = q.get()       # 從隊列讀取數據
            print('讀進程接收到:', msg)
    except multiprocessing.queues.Empty:
        print('隊列爲空')


if __name__ == "__main__":
    q = multiprocessing.Queue(2)    # 大小爲2的隊列
    p1 = multiprocessing.Process(target=write_process, args=(q,))
    p2 = multiprocessing.Process(target=read_process, args=(q,))
    p1.start()
    p2.start()
    print('主進程結束')
'''
主進程結束
讀進程接收到: 1
讀進程接收到: 1
讀進程接收到: 1
'''

管道Pipe

與只能一端寫、一端讀的Queue相比,Pipe在全雙工模式下兩端可以同時進行讀寫。其構造方法Pipe()會返回管道的兩端(pipe1, pipe2), 默認指定屬性duplex=True開啓全雙工模式,這時pipe1、pipe2都可以進行讀寫。若設置duplex=False,則pipe1只能讀,pipe2只能寫。
pipe通過send()發送信息,recv()接收信息。

def process1(pipe):
    for i in range(3):
        pipe.send(i)
        time.sleep(2)
        print('進程1收到信息:', pipe.recv())


def process2(pipe):
    for i in range(3):
        pipe.send(i)
        time.sleep(2)
        print('進程2收到信息:', pipe.recv())


if __name__ == "__main__":
    (pipe1, pipe2) = multiprocessing.Pipe()  # 全雙工模式管道
    p1 = multiprocessing.Process(target=process1, args=(pipe1,))
    p2 = multiprocessing.Process(target=process2, args=(pipe2,))
    p1.start()
    p2.start()
'''
進程2收到信息: 0
進程1收到信息: 0
進程1收到信息: 1
進程2收到信息: 1
進程2收到信息: 2
進程1收到信息: 2
'''

4、進程池Pool

當進程數量較少時,我們可以手動創建分配進程的執行,但是當進程數量較多時,手動分配就不太現實,而且處理器同時可以執行的進程數是有限制的,這時可以使用進程池來自動分配進程資源。當進程池中有空閒的進程資源時,就會自動分配給請求並執行,如果池中的進程數已經達到規定最大值,那麼該請求就會等待,直到池中有進程結束,纔會分配新的進程給請求。

通過Pool()來創建進程池,processes屬性指定最大進程池數。通過apply_async()以異步的方式分配進程,apply()以阻塞的方式。close()關閉進程池

如下所示,首先創建大小爲2的進程池,然後循環提交3個進程請求,由於進程池大小爲2,進程0、1先執行,結束後,進程池再分配資源執行進程2

def process(i):
    print('執行進程:', i)
    time.sleep(2)
    print('進程', i, '執行結束')


if __name__ == "__main__":
    pool = multiprocessing.Pool(processes=2)  # 創建大小爲2的進程池
    for i in range(3):
        pool.apply_async(func=process, args=(i,))  # 異步的方式分配進程

    print('主進程繼續執行')
    pool.close()	# 關閉進程池
    pool.join()  # 阻塞主進程直到進程池運行結束
    print('主進程執行結束')
'''
主進程繼續執行
執行進程: 0
執行進程: 1
進程 0 執行結束
進程 1 執行結束
執行進程: 2
進程 2 執行結束
主進程執行結束
'''

所謂異步調用是指發起任務後必須不用等待執行任務,可以立即開啓執行其他操作,相對地同步調用是發起任務後必須在原地等待任務執行完成,才能繼續執行。上面的例子中使用apply_async()以異步的方式分配進程,可見分配進程之後主進程不等待子進程的執行,輸出“主進程繼續執行”。當採用apply()分配進程時,執行順序如下,可見進程0開始執行後並沒有繼續執行其他進程,而是等待執行結束後才繼續執行進程1.這樣的方式並不能利用多進程併發的優勢。

執行進程: 0
進程 0 執行結束
執行進程: 1
進程 1 執行結束
執行進程: 2
進程 2 執行結束
主進程繼續執行
主進程執行結束

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