Python使用multiprocessing實現多程序

hihibig發表於2024-12-04

Python多程序是一種並行程式設計模型,允許在Python程式中同時執行多個程序。每個程序都擁有自己的獨立記憶體空間和執行環境,可以並行地執行任務,從而提高程式的效能和效率。

優點:

並行處理:多程序可以同時執行多個任務,充分利用多核處理器的能力,實現並行處理。這可以顯著提高程式的效能和效率,特別是在處理密集型任務或需要大量計算的場景中。

獨立性:每個程序都有自己的獨立地址空間和執行環境,程序之間互不干擾。這意味著每個程序都可以獨立地執行任務,不會受到其他程序的影響。這種獨立性使得多程序程式設計更加健壯和可靠。

記憶體隔離:由於每個程序都擁有自己的地址空間,多程序之間的資料是相互隔離的。這意味著不同程序之間的變數和資料不會相互影響,減少了資料共享和同步的複雜性。

故障隔離:如果一個程序崩潰或出現錯誤,不會影響其他程序的執行。每個程序是獨立的實體,一個程序的故障不會對整個程式產生致命影響,提高了程式的穩定性和容錯性。

可移植性:多程序程式設計可以在不同的作業系統上執行,因為程序是作業系統提供的基本概念。這使得多程序程式設計具有很好的可移植性,可以在不同的平臺上部署和執行。

缺點:

資源消耗:每個程序都需要獨立的記憶體空間和系統資源,包括開啟的檔案、網路連線等。多程序程式設計可能會增加系統的資源消耗,尤其是在建立大量程序時。

上下文切換開銷:在多程序程式設計中,程序之間的切換需要儲存和恢復程序的執行環境,這涉及到上下文切換的開銷。頻繁的程序切換可能會導致額外的開銷,影響程式的效能。

資料共享與同步:由於多程序之間的資料是相互隔離的,需要透過特定的機制進行資料共享和同步。這可能涉及到程序間通訊(IPC)的複雜性,如佇列、管道、共享記憶體等。正確處理資料共享和同步是多程序程式設計中的挑戰之一。

程式設計複雜性:相比於單執行緒或多執行緒程式設計,多程序程式設計可能更加複雜。需要考慮程序的建立和管理、程序間通訊、資料共享和同步等問題。編寫和除錯多程序程式可能需要更多的工作和經驗。

程序與執行緒:

在討論多程序之前,需要明確程序(Process)和執行緒(Thread)的概念。
程序是計算機中正在執行的程式的例項。每個程序都有自己的地址空間、資料棧和控制資訊,可以獨立執行任務。
執行緒是程序中的一個執行單元,可以看作是輕量級的程序。多個執行緒共享同一程序的資源,包括記憶體空間、檔案描述符等。
多程序程式設計在並行處理和資源隔離方面具有明顯的優勢,但也涉及到資源消耗、上下文切換開銷、資料共享和同步等問題。在實際應用中,開發者應權衡利弊,根據具體場景選擇適合的程式設計模型和工具。


二、建立程序
在Python中,可以使用multiprocessing模組來建立和管理程序。該模組提供了豐富的類和函式,用於建立、啟動和管理程序。

1、匯入multiprocessing模組
在使用multiprocessing模組之前,需要先匯入它:

import multiprocessing

2、建立程序
可以使用multiprocessing.Process類來建立程序物件。需要傳入一個目標函式作為程序的執行邏輯。可以透過繼承multiprocessing.Process類來自定義程序類。

import multiprocessing
 
def worker():
    # 程序執行的邏輯
 
if __name__ == '__main__':
    process = multiprocessing.Process(target=worker)

在上面的示例中,worker函式是程序的執行邏輯。程序物件建立後,可以透過設定引數、呼叫方法等來配置程序。


3、啟動程序
透過呼叫程序物件的start()方法,可以啟動程序。程序會在後臺開始執行。

process.start()

4、程序的狀態
程序物件提供了一些方法來獲取和管理程序的狀態:

is_alive():檢查程序是否正在執行。
join([timeout]):等待程序結束。可選引數timeout指定等待的最長時間。

if process.is_alive():
    print("程序正在執行")
 
process.join()

二、程序間通訊
程序間通訊(Inter-Process Communication,IPC)是指不同程序之間進行資料交換和共享資訊的機制。在多程序程式設計中,程序之間通常需要進行資料傳輸、共享狀態或進行同步操作。Python提供了多種程序間通訊的機制,包括佇列(Queue)、管道(Pipe)、共享記憶體(Value、Array)等。


1、佇列(Queue)
佇列是一種常用的程序間通訊方式,透過佇列可以實現程序之間的資料傳輸。Python的multiprocessing模組提供了Queue類來實現多程序之間的佇列通訊。程序可以透過put()方法將資料放入佇列,其他程序則可以透過get()方法從佇列中獲取資料

from multiprocessing import Queue
 
# 建立佇列
queue = Queue()
 
# 程序1放入資料
queue.put(data)
 
# 程序2獲取資料
data = queue.get()

2、管道(Pipe)
管道是另一種常用的程序間通訊方式,透過管道可以實現程序之間的雙向通訊。Python的multiprocessing模組提供了Pipe類來建立管道物件。Pipe()方法返回兩個連線的管道端,一個用於傳送資料,另一個用於接收資料。

from multiprocessing import Pipe
 
# 建立管道
conn1, conn2 = Pipe()
 
# 程序1傳送資料
conn1.send(data)
 
# 程序2接收資料
data = conn2.recv()

3、共享記憶體(Value、Array)
共享記憶體是一種在多程序之間共享資料的高效方式。Python的multiprocessing模組提供了Value和Array類來實現程序間共享資料。Value用於共享單個值,而Array用於共享陣列。

from multiprocessing import Value, Array
 
# 建立共享值
shared_value = Value('i', 0)
 
# 建立共享陣列
shared_array = Array('i', [1, 2, 3, 4, 5])

在建立共享值和共享陣列時,需要指定資料型別(如整數、浮點數)和初始值。程序可以透過讀寫共享值和共享陣列來進行程序間的資料共享。

4、訊號量(Semaphore)
訊號量是一種用於控制對共享資源的訪問的機制。在多程序程式設計中,訊號量可以用於限制同時訪問某個共享資源的程序數量。

from multiprocessing import Semaphore, Process
import time
 
def worker(semaphore, name):
    semaphore.acquire()
    print("Worker", name, "acquired semaphore")
    time.sleep(2)
    print("Worker", name, "released semaphore")
    semaphore.release()
 
semaphore = Semaphore(2)
 
processes = []
for i in range(5):
    p = Process(target=worker, args=(semaphore, i))
    processes.append(p)
    p.start()
 
for p in processes:
    p.join()

在上述例子中,建立了一個訊號量,初始值為2。然後建立了5個程序,每個程序在執行前會嘗試獲取訊號量,如果訊號量的值大於0,則成功獲取;否則,程序將被阻塞,直到有程序釋放訊號量。每個程序獲取訊號量後,會執行一段任務,並在執行完後釋放訊號量。

5、事件(Event)
事件是一種用於多程序間通訊的同步機制,它允許一個或多個程序等待某個事件的發生,然後再繼續執行。

from multiprocessing import Event, Process
import time
 
def worker(event, name):
    print("Worker", name, "waiting for event")
    event.wait()
    print("Worker", name, "received event")
    time.sleep(2)
    print("Worker", name, "completed task")
 
event = Event()
 
processes = []
for i in range(3):
    p = Process(target=worker, args=(event, i))
    processes.append(p)
    p.start()
 
time.sleep(3)
event.set()
 
for p in processes:
    p.join()

在上述例子中,建立了一個事件。然後建立了3個程序,每個程序在執行前會等待事件的發生,即呼叫event.wait()方法。主程序休眠3秒後,設定事件的狀態為已發生,即呼叫event.set()方法。此時,所有等待事件的程序將被喚醒,並繼續執行任務。

6、條件變數(Condition)
條件變數是一種用於多程序間協調和同步的機制,它可以用於控制多個程序之間的執行順序。

from multiprocessing import Condition, Process
import time
 
def consumer(condition):
    with condition:
        print("Consumer is waiting")
        condition.wait()
        print("Consumer is consuming the product")
 
def producer(condition):
    with condition:
        time.sleep(2)
        print("Producer is producing the product")
        condition.notify()
 
condition = Condition()
 
consumer_process = Process(target=consumer, args=(condition,))
producer_process = Process(target=producer, args=(condition,))
 
consumer_process.start()
producer_process.start()
 
consumer_process.join()
producer_process.join()

在上述例子中,建立了一個條件變數。然後建立了一個消費者程序和一個生產者程序。消費者程序在執行前等待條件的滿足,即呼叫condition.wait()方法。生產者程序休眠2秒後,生成產品並透過condition.notify()方法通知消費者。消費者收到通知後繼續執行任務。

三、程序間同步
程序間同步是確保多個程序按照特定順序執行或在共享資源上進行互斥訪問的一種機制。程序間同步的目的是避免競態條件(race condition)和資料不一致的問題。Python提供了多種機制來實現程序間的同步,包括鎖(Lock)、訊號量(Semaphore)、事件(Event)、條件變數(Condition)等。

1、鎖(Lock)
鎖是一種最基本的同步機制,用於保護共享資源的互斥訪問,確保在任意時刻只有一個程序可以訪問共享資源。在Python中,可以使用multiprocessing模組的Lock類來實現鎖。

from multiprocessing import Lock, Process
 
lock = Lock()
 
def worker(lock, data):
    lock.acquire()
    try:
        # 對共享資源進行操作
        pass
    finally:
        lock.release()
 
processes = []
for i in range(5):
    p = Process(target=worker, args=(lock, i))
    processes.append(p)
    p.start()
 
for p in processes:
    p.join()

在上述例子中,每個程序在訪問共享資源之前會先獲取鎖,然後在完成操作後釋放鎖。這樣可以確保在同一時刻只有一個程序能夠訪問共享資源,避免資料競爭問題。

2、訊號量(Semaphore)
訊號量是一種更為靈活的同步機制,它允許多個程序同時訪問某個資源,但限制同時訪問的程序數量。在Python中,可以使用multiprocessing模組的Semaphore類來實現訊號量。

from multiprocessing import Semaphore, Process
 
semaphore = Semaphore(2)
 
def worker(semaphore, data):
    semaphore.acquire()
    try:
        # 對共享資源進行操作
        pass
    finally:
        semaphore.release()
 
processes = []
for i in range(5):
    p = Process(target=worker, args=(semaphore, i))
    processes.append(p)
    p.start()
 
for p in processes:
    p.join()

在上述例子中,建立了一個初始值為2的訊號量。每個程序在訪問共享資源之前會嘗試獲取訊號量,只有當訊號量的值大於0時才能獲取成功,否則程序將被阻塞。獲取成功後,程序可以進行操作,並在完成後釋放訊號量。

3、事件(Event)
事件是一種同步機制,用於實現程序之間的等待和通知機制。一個程序可以等待事件的發生,而另一個程序可以觸發事件的發生。在Python中,可以使用multiprocessing模組的Event類來實現事件。

from multiprocessing import Event, Process
 
event = Event()
 
def worker(event, data):
    event.wait()
    # 執行任務
 
processes = []
for i in range(5):
    p = Process(target=worker, args=(event, i))
    processes.append(p)
    p.start()
 
# 觸發事件的發生
event.set()
 
for p in processes:
    p.join()

在上述例子中,多個程序在執行任務前會等待事件的發生,即呼叫event.wait()方法。主程序透過呼叫event.set()方法來觸發事件的發生,進而喚醒等待的程序繼續執行。

4、條件變數(Condition)
條件變數是一種複雜的同步機制,它允許程序按照特定的條件等待和通知。在Python中,可以使用multiprocessing模組的Condition類來實現條件變數。

from multiprocessing import Condition, Process
 
condition = Condition()
 
def consumer(condition(續):
def consumer(condition, data):
    with condition:
        while True:
            # 檢查條件是否滿足
            while not condition_is_met():
                condition.wait()
            # 從共享資源中消費資料
def producer(condition, data):
    with condition:
        # 生成資料並更新共享資源
        condition.notify_all()
processes = []
for i in range(5):
    p = Process(target=consumer, args=(condition, i))
    processes.append(p)
    p.start()
producer_process = Process(target=producer, args=(condition, data))
producer_process.start()
for p in processes:
    p.join()
producer_process.join()

在上述例子中,消費者程序在執行任務前會檢查條件是否滿足,如果條件不滿足,則呼叫condition.wait()方法等待條件的滿足。生產者程序生成資料並更新共享資源後,呼叫condition.notify_all()方法通知所有等待的消費者程序條件已滿足。被喚醒的消費者程序會重新檢查條件並執行任務。

四、程序池
程序池是一種用於管理和排程多個程序的機制,它可以有效地處理並行任務和提高程式的效能。程序池在Python中通常使用multiprocessing模組提供的Pool類來實現。

程序池的工作原理如下:

建立程序池時,會啟動指定數量的程序,並將它們放入池中。
池中的程序會等待主程序提交任務。
主程序透過提交任務給程序池,將任務分配給空閒的程序。
程序池中的程序執行任務,並將結果返回給主程序。
主程序獲取任務的結果,繼續執行其他操作。
當所有任務完成後,主程序關閉程序池。

1、建立程序池
要使用程序池,首先需要建立一個Pool物件,可以指定池中的程序數量。通常,可以使用multiprocessing.cpu_count()函式來獲取當前系統的CPU核心數,然後根據需要來指定程序池的大小。

from multiprocessing import Pool, cpu_count
 
pool = Pool(processes=cpu_count())

在上述例子中,建立了一個程序池,程序數量與系統的CPU核心數相同。

2、提交任務
一旦建立了程序池,就可以使用apply()、map()或imap()方法來提交任務給程序池。

apply()方法用於提交單個任務,並等待任務完成後返回結果。

result = pool.apply(function, args=(arg1, arg2))

map()方法用於提交多個任務,並按照任務提交的順序返回結果列表。

results = pool.map(function, iterable)

 imap()方法也用於提交多個任務,但可以透過迭代器逐個獲取結果,而不需要等待所有任務完成。

results = pool.imap(function, iterable)

在上述例子中,function表示要執行的函式,args是函式的引數,iterable是一個可迭代物件,可以是列表、元組等。

3、獲取結果
對於apply()方法,呼叫後會阻塞主程序,直到任務完成並返回結果。對於map()方法,呼叫後會等待所有任務完成,並按照任務提交的順序返回結果列表。對於imap()方法,可以透過迭代器逐個獲取結果。

for result in results:

    print(result)

在上述例子中,使用for迴圈逐個獲取結果並進行處理。

4、關閉程序池
在所有任務完成後,需要顯式地關閉程序池,以釋放資源。

pool.close()
pool.join()

呼叫close()方法後,程序池將不再接受新的任務。呼叫join()方法會阻塞主程序,直到所有任務都已完成。

5、使用程序池的示例

from multiprocessing import Pool
 
# 定義一個任務函式
def square(x):
    return x ** 2
 
if __name__ == '__main__':
    # 建立程序池
    with Pool(processes=4) as pool:
        # 提交任務給程序池
        results = pool.map(square, range(10))
 
    # 列印結果
    print(results)

在上述示例中,首先定義了一個任務函式square,它接受一個數值作為引數,並返回該數值的平方。

在if __name__ == '__main__':中,建立了一個程序池,指定程序數量為4。使用with語句可以確保程序池在使用完畢後被正確關閉。

然後,透過pool.map(square, range(10))將任務提交給程序池。map()方法會將任務函式square和一個可迭代物件range(10)作為引數,它會將可迭代物件中的每個元素依次傳遞給任務函式進行處理,並返回結果列表。最後,列印結果列表,即每個數值的平方。

需要注意的是,在使用程序池時,需要將主程式程式碼放在if __name__ == '__main__':中,以確保在子程序中不會重複執行主程式的程式碼。

以下是一個更加複雜的多程序示例,展示瞭如何使用程序池處理多個任務,並在任務完成時獲取結果。

import time
from multiprocessing import Pool
 
# 定義一個任務函式
def process_data(data):
    # 模擬耗時操作
    time.sleep(1)
    # 返回處理結果
    return data.upper()
 
if __name__ == '__main__':
    # 建立程序池
    with Pool(processes=3) as pool:
        # 準備資料
        data_list = ['apple', 'banana', 'cherry', 'date', 'elderberry']
 
        # 提交任務給程序池
        results = [pool.apply_async(process_data, args=(data,)) for data in data_list]
 
        # 等待所有任務完成並獲取結果
        final_results = [result.get() for result in results]
 
    # 列印結果
    for result in final_results:
        print(result)

在上述示例中,除了使用程序池的map()方法提交任務之外,還使用了apply_async()方法來非同步提交任務,並透過get()方法獲取任務的結果。

在if __name__ == '__main__':中,建立了一個程序池,指定程序數量為3。使用with語句可以確保程序池在使用完畢後被正確關閉。然後,準備了一個資料列表data_list,其中包含了需要處理的資料。

透過列表推導式,使用pool.apply_async(process_data, args=(data,))將任務非同步提交給程序池。apply_async()方法會將任務函式process_data和資料data作為引數,返回一個AsyncResult物件,表示非同步任務的結果。將這些物件儲存在results列表中。

接下來,使用列表推導式,透過result.get()方法等待所有任務完成並獲取結果,將結果儲存在final_results列表中。最後,使用for迴圈遍歷final_results列表,並列印每個任務的處理結果。

程序池的優點是可以自動管理和排程多個程序,充分利用系統資源,提高程式的並行執行能力。透過合理設定程序池的大小,可以在不過度消耗系統資源的情況下,實現最佳的併發效果。但需要注意的是,程序池適用於那些需要並行執行的任務,而不適用於IO密集型任務,因為程序池中的程序是透過複製主程序來建立的,而IO密集型任務更適合使用執行緒池來實現併發。

相關文章