進程間通信(Python:Queue,Pipe,Value..)

進程間通信(Python:Queue,Pipe,Value…)



前言

與多線程不同,多進程之間不會共享全局變量,所以多進程通信需要藉助“外力”。在Python中,這些常用的外力有Queue,Pipe,Value/Array和Manager。

Queue

這裏的Queue不是queue模塊中的Queue——它在多進程中無法起到通信作用,我們需要multiprocessing模塊下的。同時,由於Python的完美封裝,它的實現原理可以說是對程序員完全透明,使用者把它當作尋常隊列使用即可。就像下面這個生產者/消費者的demo一樣,二者通過queue互通往來。

import random
import multiprocessing

def producer(queue):  # 生產者生產數據
    for __ in range(10):
        queue.put(random.randrange(100))

def consumer(queue):  # 消費者處理數據
    while True:
        if not queue.empty():
            item = queue.get()  # 模擬消費者的處理過程
            print("處理一個元素:{}".format(item))

if __name__ == "__main__":
    queue = multiprocessing.Queue()

    proProcess = multiprocessing.Process(target=producer, args=(queue,))
    conProcess = multiprocessing.Process(target=consumer, args=(queue,))
    proProcess.start()
    conProcess.start()

    proProcess.join()
    while not queue.empty():  # 當隊列不爲空時,繼續等待消費者處理
        pass
    conProcess.terminate()  # 終止消費者進程
    print("處理結束")

# 輸出:
處理一個元素:14
處理一個元素:90
處理一個元素:72
處理一個元素:84
處理一個元素:21
處理一個元素:43
處理一個元素:52
處理一個元素:79
處理一個元素:95
處理一個元素:73
處理結束

Pipe

Queue適用於絕大多數場景,爲滿足普遍性而不得不多方考慮,它因此顯得“重”。Pipe更爲輕巧,速度更快。它的使用如同Socket編程裏的套接字,通過recv()send()實現通信機制。使用方法:

import multiprocessing

sender, reciver = multiprocessing.Pipe()

其實查看Pipe的源碼會發現,Pipe()方法返回兩個 Connection() 實例,也就是說返回的兩個對象完全一樣(但id不一樣),只不過我們用不同的變量名做了區分。

# Pipe源碼
def Pipe(duplex=True):
    return Connection(), Connection()

send()方法可以不停發送數據,可以看作是它把數據送到一個容器中,而recv()方法就是從這個容器裏取數據,當容器中沒有數據後,recv()會阻塞當前進程。需要注意的是:recv不能取同一個對象send出去的數據。

import multiprocessing

if __name__ == "__main__":
    sender, reciver = multiprocessing.Pipe()

    sender.send("zty")  # sender發數據
    data = reciver.recv()  # reciver取數據
    print(data)  # 輸出:zty

    sender.send("zty")  # sender發數據
    data = sender.recv()  # sender取數據,但程序被阻塞,因爲recv不能取同一個對象send出去的數據
    print(data)

將Queue中的demo用Pipe修改,代碼成了下邊這樣:

import time
import random
import multiprocessing

def producer(pro):  # 生產者生產數據
    for __ in range(10):
        pro.send(random.randrange(100))

def consumer(con):  # 消費者處理數據
    while True:
        data = con.recv()
        print("處理一個元素:{}".format(data))

if __name__ == "__main__":
    pro, con = multiprocessing.Pipe()

    proProcess = multiprocessing.Process(target=producer, args=(pro,))
    conProcess = multiprocessing.Process(target=consumer, args=(con,))
    proProcess.start()
    conProcess.start()

    proProcess.join()
    time.sleep(2)  # 確保數據處理完後終止消費者
    conProcess.terminate()  # 由於recv會阻塞進程,所以手動終止
    print("處理結束")

Value/Array

multiprocessing.Valuemultiprocessing.Array的實現基於內存共享,這裏簡單介紹如何使用。

# 抽象出的Value和Array源碼
def Value(typecode_or_type, *args, **kwargs):
    pass

def Array(typecode_or_type, size_or_initializer, lock=True):
    pass

無論是Value()還是Array(),第一個參數都是typecode_or_type。type_code表示類型碼,在Python中已經預先設計好了,如”c“表示char類型,“i”表示singed int類型,“f”表示float類型,等等(更多可見這篇Python:線程、進程與協程(5)——multiprocessing模塊(2))。但我覺得這種方式不易記憶,更偏愛用type表達類型。這裏需要藉助ctypes模塊。

ctypes.c_char   ==>  字符型
ctypes.c_int    ==>  整數型
ctypes.c_float  ==>  浮點型

兩種使用方式的比較:

# typecode
nt_typecode = Value("i", 512)
float_typecode = Value("f", 1024.0)
char_typecode = Value("c", b"a")  # 第二個參數是byte型

# type
import ctypes
int_type = Value(ctypes.c_int, 512)
float_type = Value(ctypes.c_float, 1024.0)
char_type = Value(ctypes.c_char, b"a")  # 第二個參數是byte型

有幾點需要注意:

  • 對於Value的對象來說,需要通過.value獲取屬性值;
  • Array中的第一個參數表示:該數組中存放的元素的類型;
  • 如果需要字符串,通過Array實現,而不是Value。

Array()第二個參數是size_or_initializer,表示傳入參數可以是數組的長度,或者初始化值。這裏的Array是地地道道的數組,而非Python中的列表,有過C語言經驗的人應該可以立馬明白。

使用方式如下:

from multiprocessing import Process, Value, Array

def producer(num, string):
    num.value = 1024

    string[0] = b"z"  # 只能一個一個的賦值
    string[1] = b"t"
    string[2] = b"y"

def consumer(num, string):
    print(num.value)
    print(b"".join(string))

if __name__ == "__main__":
    import ctypes
    num = Value(ctypes.c_int, 512)
    string = Array(ctypes.c_char, 3)  # 設置一個長度爲3的數組

    proProcess = Process(target=producer, args=(num, string))
    conProcess = Process(target=consumer, args=(num, string))
	...

# 輸出:
1024
b'zty'

Manager

Manager是通過共享進程的方式共享數據,它支持的數據類型比Value和Array更豐富。單拿Manager中的Value來說,它就直接支持字符串:

def producer(num, string):
    num.value = 1024
    string.value = "zty"  # 支持字符串賦值

def consumer(num, string):
    print(num.value)
    print(string.value)

if __name__ == "__main__":
    import ctypes
    num = Manager().Value(ctypes.c_int, 512)
    string = Manager().Value(ctypes.c_char, "")
    
    proProcess = Process(target=producer, args=(num, string))
    conProcess = Process(target=consumer, args=(num, string))
    ...

# 輸出:
1024
zty

但Manager中的Array似乎有被削弱的感覺。

首先,它的第一個參數不再支持type方式。如果你強制使用,會得到這樣的報錯:TypeError: array() argument 1 must be a unicode character, not _ctypes.PyCSimpleType
其次,它允許的類型也變少了。傳入的typecode必須在b, B, u, h, H, i, I, l, L, q, Q, f or d之中——很明顯,它不支持char類型了。

總的來說,Manager已經足夠強大,它還支持Lock,RLock等操作,這些操作與線程中的一般無二,這是因爲它們是藉助threading模塊實現的。

dict_ = Manager().dict()  # 字典對象
queue = Manager().Queue()  # 隊列

lock = Manager().Lock()  # 普通鎖
rlock = Manager().RLock()  # 可衝入鎖
cond = Manager().Condition()  # 條件鎖
semaphore = Manager().Semaphore()  # 信號鎖
event = Manager().Event()  # 事件鎖

namespace = Manager().Namespace()  # 命名空間

需要重點介紹的是Manager().Namespace()。它會開闢一個空間,在這個命名空間中,可以更“隨性”使用Python中的數據類型,訪問這個空間只需要對象名.xxx即可。像下面這樣:

from multiprocessing import Process, Manager

def producer(namespace):  # 生產者生產數據
    namespace.name = "zty"
    namespace.info = {"Id": 12345, "Addr": "chengdu"}
    namespace.age = 19

def consumer(namespace):
    import time
    time.sleep(1)
    print(namespace.name)
    print(namespace.info)
    print(namespace.age)


if __name__ == "__main__":
    namespace = Manager().Namespace()

    proProcess = Process(target=producer, args=(namespace,))
    conProcess = Process(target=consumer, args=(namespace,))
	...

# 輸出:
zty
{'Id': 12345, 'Addr': 'chengdu'}
19

不過它有一個缺點:無法直接修改可變類型的數據。拿list舉例,即便是在一個子進程中修改了命名空間中列表的值,然而在另一個子進程中獲取這個列表,得到的依然是未修改之前的數據。

def producer(namespace):
    namespace.nums[2] = 3  # nums = [5, 1, 3]

def consumer(namespace):
    time.sleep(1)
    print(namespace.nums)  # 輸出:[5, 1, 2]

if __name__ == "__main__":
    namespace = Manager().Namespace()

    namespace.nums = [5, 1, 2]
    namespace.alphas = ["z", "t", "y"]
    proProcess = Process(target=producer, args=(namespace,))
    conProcess = Process(target=consumer, args=(namespace,))
    ...

解決方法,更新列表引用(重新賦值):

def producer(namespace):  # 生產者生產數據
    nums = namespace.nums
    nums[2] = 3
    namespace.nums = nums

def consumer(namespace):
    time.sleep(1)
    print(namespace.nums)  # 輸出:513

詳情請見:How does multiprocessing.Manager() work in python?


感謝

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