Python多进程(process)(二)进程间通讯

Python实用教程_spiritx的博客-CSDN博客

前面一篇对子进程的创建、进程池的管理进行了学习,在实践中,如果可能,尽可能使用进程池的提交方式将数据传递给子进程,再使用返回值收集的方式将子进程处理的结果收集上来,这样处理既安全又比较优雅。

但有些场景需要子进程或者父子进程之间进行比较复杂的数据交换,这就需要使用进程间通讯来实现。

进程与线程不一样,线程之间可以通过进程内部的共享数据进行数据的访问和共享,但一个进程如果要访问其他进程的信息,需要通过操作系统提供的方法进行访问。

管道pipe()

定义:

multiprocessing.Pipe([duplex])

返回一对 Connection 对象 (conn1, conn2) , 分别表示管道的两端。
如果 duplex 被置为 True (默认值),那么该管道是双向的。如果 duplex 被置为 False ,那么该管道是单向的,即 conn1 只能用于接收消息,而 conn2 仅能用于发送消息。

Connection 对象

定义:

class multiprocessing.connection.Connection

主要属性和方法

属性和方法说明
send(obj)将一个对象发送到连接的另一端,可以用 recv() 读取。
发送的对象必须是可以序列化的,过大的对象 ( 接近 32MiB+ ,这个值取决于操作系统 ) 有可能引发 ValueError  异常。
recv()返回一个由另一端使用 send() 发送的对象。该方法会一直阻塞直到接收到对象。 如果对端关闭了连接或者没有东西可接收,将抛出 EOFError 异常。
fileno()返回由连接对象使用的描述符或者句柄。
close()关闭连接对象。
当连接对象被垃圾回收时会自动调用。
poll([timeout])返回连接对象中是否有可以读取的数据。
如果未指定 timeout ,此方法会马上返回。如果 timeout 是一个数字,则指定了最大阻塞的秒数。如果 timeout 是 None,那么将一直等待,不会超时。
注意通过使用 multiprocessing.connection.wait() 可以一次轮询多个连接对象。
send_bytes(buffer[, offset[, size]])从一个 bytes-like object 对象中取出字节数组并作为一条完整消息发送。
如果由 offset  给定了在 buffer 中读取数据的位置。 如果给定了 size ,那么将会从缓冲区中读取多个字节。 过大的缓冲区 ( 接近 32MiB+ ,此值依赖于操作系统 ) 有可能引发 ValueError  异常。
recv_bytes([maxlength])以字符串形式返回一条从连接对象另一端发送过来的字节数据。此方法在接收到数据前将一直阻塞。 如果连接对象被对端关闭或者没有数据可读取,将抛出 EOFError 异常。
如果给定了 maxlength 并且消息长于 maxlength 那么将抛出 OSError 并且该连接对象将不再可读。
在 3.3 版更改: 曾经该函数抛出 IOError ,现在这是 OSError 的别名。
recv_bytes_into(buffer[, offset])将一条完整的字节数据消息读入 buffer 中并返回消息的字节数。 此方法在接收到数据前将一直阻塞。 如果连接对象被对端关闭或者没有数据可读取,将抛出 EOFError  异常。
如果缓冲区太小,则将引发 BufferTooShort  异常,并且完整的消息将会存放在异常实例 e 的 e.args[0] 中。
import os, time
import multiprocessing
import multiprocessing.connection


def recv(conn:multiprocessing.connection.Connection):
    while(conn.poll(3)):
        data = conn.recv()
        print(f'子进程{os.getpid()}, {__name__=} recv: {data=}')
        conn.send(data.upper())
    conn.close()
    print('子进程长时间未收到数据.')

class MyChildProcess(multiprocessing.Process):
    def __init__(self, conn):
        multiprocessing.Process.__init__(self)
        self.conn = conn

    def run(self):
        recv(self.conn)


if __name__ == '__main__':
    conn1,conn2 = multiprocessing.Pipe()
    p1 = MyChildProcess(conn1)
    p1.start()
    for data in ['hello', 'statelessness', 'retain', 'birth']:
        conn2.send(data)
        rcvdata = conn2.recv()
        print(f'主进程 recv():{rcvdata}')
    time.sleep(4)
    conn2.close()
    p1.join()


‘’'
子进程80808, __name__='__mp_main__' recv: data='hello'
主进程 recv():HELLO
子进程80808, __name__='__mp_main__' recv: data='statelessness'
主进程 recv():STATELESSNESS
子进程80808, __name__='__mp_main__' recv: data='retain'
主进程 recv():RETAIN
子进程80808, __name__='__mp_main__' recv: data='birth'
主进程 recv():BIRTH
子进程长时间未收到数据.
‘''

管道的特点:

  • 只能用于具有亲缘关系的进程之间的通信(也是父子进程或者兄弟进程之间)。
  • 它可以看成是一种特殊的文件,对于它的读写也可以使用普通的 read、write 等函数。但是它不是普通的文件,并不属于其他任何文件系统,并且只存在于内存中。

管道的并发问题

可以支持多个进程进行读写,但需要配合锁和信号量来使用,否则会容易出现争抢异常而卡死的情况

import os, time
import multiprocessing
import multiprocessing.connection


def recv(connrecv:multiprocessing.connection.Connection,
         connsend:multiprocessing.connection.Connection):
    try:
        while(connrecv.poll(3)):
            print(f'子进程{os.getpid()} poll enter...')
            data = connrecv.recv()
            print(f'子进程{os.getpid()}, {__name__=} recv: {data=}')
            time.sleep(1)
            connsend.send(data.upper())
    except EOFError as e:
        pass

    print(f'子进程{os.getpid()}长时间未收到数据.')

class MyChildProcess(multiprocessing.Process):
    def __init__(self, connrecv, connsend):
        multiprocessing.Process.__init__(self)
        self.connrecv = connrecv
        self.connsend = connsend

    def run(self):
        recv(self.connrecv, self.connsend)


if __name__ == '__main__':
    psend = multiprocessing.Pipe(False) #(接收,发送)
    precv = multiprocessing.Pipe(False)
    p1 = MyChildProcess(psend[0], precv[1])
    p2 = MyChildProcess(psend[0], precv[1])
    p1.start()
    p2.start()
    for data in ['Google', 'Yahoo', 'Twtter', 'Facebook', 'Meta', 'Apple', 'Yahoo']:
        print(f'主进程 send():{data}')
        psend[1].send(data)
        rcvdata = precv[0].recv()
        time.sleep(1)
        print(f'主进程 recv():{rcvdata}')
    time.sleep(4)
    p1.join()
    p2.join()

‘’'
主进程 send():Google
子进程83549 poll enter...
子进程83549, __name__='__mp_main__' recv: data='Google'
主进程 recv():GOOGLE
主进程 send():Yahoo
子进程83549 poll enter...
子进程83550 poll enter...
子进程83550长时间未收到数据.
…(子进程83549挂死了)
‘''

上面的代码并不能总是能执行成功,两个子进程都争抢到了poll后,有一个子进程实际是读不到数据的。

即使不使用poll(),而直接使用recv(),也会出现读争抢的异常。

队列

python中有多种支持进程间通讯的队列:

Queue对象

class multiprocessing.Queue([maxsize])

返回一个使用一个管道和少量锁和信号量实现的共享队列对象。当一个进程将一个数据放进队列中时,一个写入线程会启动并将数据从缓冲区写入管道中。
一旦超时,将抛出标准库 queue 模块中常见的异常 queue.Empty 和 queue.Full。
除了 task_done() 和 join() 之外,Queue  实现了标准库类 queue.Queue 中所有的方法。

注意:Python中的队列(包括Queue、SimpleQueue等)目前只支持有父子关系的Process类,不支持任意进程之间的通讯,也不支持在Pool进程池中使用。

主要属性和方法

属性和方法说明
qsize()返回队列的大致长度。
empty()如果队列是空的,返回 True ,反之返回 False 。 由于多线程或多进程的环境,该状态是不可靠的。
full()如果队列是满的,返回 True ,反之返回 False 。 由于多线程或多进程的环境,该状态是不可靠的。
put(obj[, block[, timeout]])将 obj 放入队列。如果可选参数 block 是 True (默认值) 而且 timeout 是 None (默认值), 将会阻塞当前进程,直到有空的缓冲槽。如果 timeout 是正数,将会在阻塞了最多 timeout 秒之后还是没有可用的缓冲槽时抛出 queue.Full  异常。反之 (block 是 False 时),仅当有可用缓冲槽时才放入对象,否则抛出 queue.Full 异常 (在这种情形下 timeout 参数会被忽略)。
put_nowait(obj)相当于 put(obj, False)
get([block[, timeout]])从队列中取出并返回对象。如果可选参数 block 是 True (默认值) 而且 timeout 是 None (默认值), 将会阻塞当前进程,直到队列中出现可用的对象。如果 timeout 是正数,将会在阻塞了最多 timeout 秒之后还是没有可用的对象时抛出 queue.Empty 异常。反之 (block 是 False 时),仅当有可用对象能够取出时返回,否则抛出 queue.Empty 异常 (在这种情形下 timeout 参数会被忽略)。
在 3.8 版更改: 如果队列已经关闭,会抛出 ValueError 而不是 OSError 。
get_nowait()相当于 get(False) 
close()指示当前进程将不会再往队列中放入对象。一旦所有缓冲区中的数据被写入管道之后,后台的线程会退出。这个方法在队列被gc回收时会自动调用。
join_thread()等待后台线程。这个方法仅在调用了 close() 方法之后可用。这会阻塞当前进程,直到后台线程退出,确保所有缓冲区中的数据都被写入管道中。
cancel_join_thread()防止 join_thread() 方法阻塞当前进程。具体而言,这防止进程退出时自动等待后台线程退出。
import os, time
import multiprocessing
import multiprocessing.connection
import queue


def recv(recvq:multiprocessing.Queue, sendq:multiprocessing.Queue):
    while True:
        try:
            data = recvq.get(block=True, timeout=3)
            print(f'子进程{os.getpid()}, {__name__=} recv: {data=}')
            sendq.put(data.upper())
        except queue.Empty:
            print(f'子进程{os.getpid()}长时间未收到数据.')
            break



class MyChildProcess(multiprocessing.Process):
    def __init__(self, recvq, sendq):
        multiprocessing.Process.__init__(self)
        self.recvq = recvq
        self.sendq = sendq

    def run(self):
        recv(self.recvq, self.sendq)


if __name__ == '__main__':
    sendq = multiprocessing.Queue()
    recvq = multiprocessing.Queue()
    p1 = MyChildProcess(sendq, recvq)
    p1.start()
    for data in ['Google', 'Yahoo', 'Twtter', 'Facebook', 'Meta', 'Apple', 'Yahoo']:
        sendq.put(data)
        rcvdata = recvq.get()
        print(f'主进程 recv():{rcvdata}')
    time.sleep(4)
    p1.join()

‘’'
子进程82214, __name__='__mp_main__' recv: data='Google'
主进程 recv():GOOGLE
子进程82214, __name__='__mp_main__' recv: data='Yahoo'
主进程 recv():YAHOO
子进程82214, __name__='__mp_main__' recv: data='Twtter'
主进程 recv():TWTTER
子进程82214, __name__='__mp_main__' recv: data='Facebook'
主进程 recv():FACEBOOK
子进程82214, __name__='__mp_main__' recv: data='Meta'
主进程 recv():META
子进程82214, __name__='__mp_main__' recv: data='Apple'
主进程 recv():APPLE
子进程82214, __name__='__mp_main__' recv: data='Yahoo'
主进程 recv():YAHOO
子进程82214长时间未收到数据.
‘''

SimpleQueue对象

这是一个简化的 Queue 类的实现,很像带锁的 Pipe 。

主要方法

属性和方法说明
close()关闭队列:释放内部资源。
队列在被关闭后就不可再被使用。 例如不可再调用 get(), put() 和 empty() 等方法。
3.9 新版功能.
empty()如果队列为空返回 True ,否则返回 False 。
get()从队列中移出并返回一个对象。
put(item)将 item 放入队列。
import os, time
import multiprocessing
import multiprocessing.connection
import queue


def recv(recvq:multiprocessing.SimpleQueue, sendq:multiprocessing.SimpleQueue):
    try:
        while True:
            data = recvq.get()
            print(f'子进程{os.getpid()}, {__name__=} recv: {data=}')
            time.sleep(1)
            sendq.put(data.upper())
    except queue.Empty:
        print(f'子进程{os.getpid()}长时间未收到数据.')




class MyChildProcess(multiprocessing.Process):
    def __init__(self, recvq, sendq):
        multiprocessing.Process.__init__(self)
        self.recvq = recvq
        self.sendq = sendq

    def run(self):
        recv(self.recvq, self.sendq)


if __name__ == '__main__':
    sendq = multiprocessing.SimpleQueue()
    recvq = multiprocessing.SimpleQueue()
    p1 = MyChildProcess(sendq, recvq)
    p2 = MyChildProcess(sendq, recvq)
    p1.start()
    p2.start()
    for data in ['Google', 'Yahoo', 'Twtter', 'Facebook', 'Meta', 'Apple', 'Yahoo']:
        sendq.put(data)
        rcvdata = recvq.get()
        print(f'主进程 recv():{rcvdata}')
    time.sleep(4)
    sendq.close()
    print(f'主进程 关闭了发送队列')
    p1.kill()
    p2.kill()
    p1.join()
    p2.join()


‘’'
子进程82597, __name__='__mp_main__' recv: data='Google'
主进程 recv():GOOGLE
子进程82597, __name__='__mp_main__' recv: data='Yahoo'
主进程 recv():YAHOO
子进程82598, __name__='__mp_main__' recv: data='Twtter'
主进程 recv():TWTTER
子进程82597, __name__='__mp_main__' recv: data='Facebook'
主进程 recv():FACEBOOK
子进程82598, __name__='__mp_main__' recv: data='Meta'
主进程 recv():META
子进程82597, __name__='__mp_main__' recv: data='Apple'
主进程 recv():APPLE
子进程82598, __name__='__mp_main__' recv: data='Yahoo'
主进程 recv():YAHOO
主进程 关闭了发送队列
‘''

SimpleQueue很好的解决了管道的并发问题,在子进程退出的问题上,需要单独的开发指令。Queue对象可以使用常时间得不到消息实现正常的退出。

import os, time
import multiprocessing
import multiprocessing.connection
import queue


def recv(recvq:multiprocessing.SimpleQueue, sendq:multiprocessing.SimpleQueue):
    try:
        while True:
            data = recvq.get()
            print(f'子进程{os.getpid()}, {__name__=} recv: {data=}')
            time.sleep(1)
            sendq.put(data.upper())
    except queue.Empty:
        print(f'子进程{os.getpid()}长时间未收到数据.')




class MyChildProcess(multiprocessing.Process):
    def __init__(self, recvq, sendq):
        multiprocessing.Process.__init__(self)
        self.recvq = recvq
        self.sendq = sendq

    def run(self):
        recv(self.recvq, self.sendq)


if __name__ == '__main__':
    sendq = multiprocessing.SimpleQueue()
    recvq = multiprocessing.SimpleQueue()
    p1 = MyChildProcess(sendq, recvq)
    p2 = MyChildProcess(sendq, recvq)
    p1.start()
    p2.start()
    for data in ['Google', 'Yahoo', 'Twtter', 'Facebook', 'Meta', 'Apple', 'Yahoo']:
        sendq.put(data)
        rcvdata = recvq.get()
        print(f'主进程 recv():{rcvdata}')
    time.sleep(4)
    sendq.close()
    print(f'主进程 关闭了发送队列')
    p1.kill()
    p2.kill()
    p1.join()
    p2.join()

‘’'
子进程82769, __name__='__mp_main__' recv: data='Google'
主进程 recv():GOOGLE
子进程82770, __name__='__mp_main__' recv: data='Yahoo'
主进程 recv():YAHOO
子进程82769, __name__='__mp_main__' recv: data='Twtter'
主进程 recv():TWTTER
子进程82770, __name__='__mp_main__' recv: data='Facebook'
主进程 recv():FACEBOOK
子进程82769, __name__='__mp_main__' recv: data='Meta'
主进程 recv():META
子进程82770, __name__='__mp_main__' recv: data='Apple'
主进程 recv():APPLE
子进程82769, __name__='__mp_main__' recv: data='Yahoo'
主进程 recv():YAHOO
主进程 关闭了发送队列
‘''

JoinableQueue对象

class multiprocessing.JoinableQueue([maxsize])

JoinableQueue 类是 Queue 的子类,额外添加了 task_done() 和 join() 方法。

task_done()
指出之前进入队列的任务已经完成。由队列的消费者进程使用。对于每次调用 get() 获取的任务,执行完成后调用 task_done() 告诉队列该任务已经处理完成。
如果 join() 方法正在阻塞之中,该方法会在所有对象都被处理完的时候返回 (即对之前使用 put() 放进队列中的所有对象都已经返回了对应的 task_done() ) 。
如果被调用的次数多于放入队列中的项目数量,将引发 ValueError 异常 。

join()
阻塞至队列中所有的元素都被接收和处理完毕。
当条目添加到队列的时候,未完成任务的计数就会增加。每当消费者进程调用 task_done() 表示这个条目已经被回收,该条目所有工作已经完成,未完成计数就会减少。当未完成计数降到零的时候, join() 阻塞被解除。

JoinableQueue提供了一种机制,可以让程序控制在队列处理完成后处理其他的工作。

import os, time
import multiprocessing
import multiprocessing.connection
import queue

class MyRecvProcess(multiprocessing.Process):
    def __init__(self, recvq, sendq):
        multiprocessing.Process.__init__(self)
        self.recvq = recvq
        self.sendq = sendq

    def run(self):
        try:
            while True:
                data = self.recvq.get()
                print(f'子进程{os.getpid()}, {__name__=} recv: {data=}')
                time.sleep(1)
                self.sendq.put(data.upper())
                self.recvq.task_done()
        except queue.Empty:
            print(f'子进程{os.getpid()}长时间未收到数据.')

class MySendProcess(multiprocessing.Process):
    def __init__(self, recvq, sendq):
        multiprocessing.Process.__init__(self)
        self.recvq = recvq
        self.sendq = sendq

    def run(self) -> None:
        for data in ['Google', 'Yahoo', 'Twtter', 'Facebook', 'Meta', 'Apple', 'Yahoo']:
            self.sendq.put(data)
            rcvdata = self.recvq.get()
            print(f'进程{os.getpid()}:{rcvdata}')
            self.recvq.task_done()

if __name__ == '__main__':
    q1 = multiprocessing.JoinableQueue()
    q2 = multiprocessing.JoinableQueue()
    p0 = MySendProcess(q1, q2) #q1->recv q2->send
    p1 = MyRecvProcess(q2, q1) #q2->recv q1->send
    p2 = MyRecvProcess(q2, q1)
    p0.start()
    p1.start()
    p2.start()

    p0.join() #等待发送进程处理完
    print('发送进程执行完!')
    q2.join() #等待所有队列中的任务都完成
    q1.join()
    print(f'主进程 关闭了发送队列')
    p1.kill()
    p2.kill()

‘’'
子进程84481, __name__='__mp_main__' recv: data='Google'
进程84479:GOOGLE
子进程84480, __name__='__mp_main__' recv: data='Yahoo'
进程84479:YAHOO
子进程84481, __name__='__mp_main__' recv: data='Twtter'
进程84479:TWTTER
子进程84480, __name__='__mp_main__' recv: data='Facebook'
进程84479:FACEBOOK
子进程84481, __name__='__mp_main__' recv: data='Meta'
进程84479:META
子进程84480, __name__='__mp_main__' recv: data='Apple'
进程84479:APPLE
子进程84481, __name__='__mp_main__' recv: data='Yahoo'
进程84479:YAHOO
发送进程执行完!
主进程 关闭了发送队列
‘''

同步原语

多进程提供了多种同步原语的对象,与线程中的使用非常相似。

注意:实践发现,同步原语不能在进程池中使用,会提示RuntimeError: objects should only be shared between processes through inheritance。

Lock

class multiprocessing.Lock

一旦一个进程拿到了锁,后续的任何其他进程的其他请求都会被阻塞直到锁被释放。任何进程都可以释放锁。

注意 Lock 实际上是一个工厂函数。它返回由默认上下文初始化的multiprocessing.synchronize.Lock  对象。

主要方法

acquire(block=True, timeout=None)
可以阻塞或非阻塞地获得锁。

  • 如果 block 参数被设为 True ( 默认值 ) , 对该方法的调用在锁处于释放状态之前都会阻塞,然后将锁设置为锁住状态并返回 True 。需要注意的是第一个参数名与 threading.Lock.acquire() 的不同。
  • 如果 block 参数被设置成 False ,方法的调用将不会阻塞。 如果锁当前处于锁住状态,将返回 False ; 否则将锁设置成锁住状态,并返回 True 。
  • 当 timeout 是一个正浮点数时,会在等待锁的过程中最多阻塞等待 timeout 秒,当 timeout 是负数时,效果和 timeout 为0时一样,当 timeout 是 None (默认值)时,等待时间是无限长。需要注意的是,对于 timeout 参数是负数和 None 的情况, 其行为与 threading.Lock.acquire() 是不一样的。当 block 参数 为 False 时, timeout 并没有实际用处,会直接忽略。否则,函数会在拿到锁后返回 True 或者 超时没拿到锁后返回 False 。

release()
释放锁,可以在任何进程、线程使用,并不限于锁的拥有者。
当尝试释放一个没有被持有的锁时,会抛出 ValueError 异常,除此之外其行为与 threading.Lock.release() 一样。

例子:

没有锁,两个进程并行执行的情况:

import os, time
import multiprocessing



def write1(lk):
    # lk.acquire()
    for i in range(5):
        print(f"write1 ...{i}")
        time.sleep(0.1)
    # lk.release()

def write2(lk):
    # lk.acquire()
    for i in range(5):
        print(f"write2 ...{i}")
        time.sleep(0.1)
    # lk.release()

if __name__ == '__main__':
    lk = multiprocessing.Lock()  # 创建锁
    w1 = multiprocessing.Process(target=write1, args=(lk,))
    w2 = multiprocessing.Process(target=write2, args=(lk,))

    w1.start()
    w2.start()
    w1.join()
    w2.join()

‘’'
write2 ...0
write1 ...0
write2 ...1
write1 ...1
write2 ...2
write1 ...2
write2 ...3
write1 ...3
write2 ...4
write1 ...4
‘''

加上锁后的情况:

import os, time
import multiprocessing



def write1(lk):
    lk.acquire()
    for i in range(5):
        print(f"write1 ...{i}")
        time.sleep(0.1)
    lk.release()

def write2(lk):
    lk.acquire()
    for i in range(5):
        print(f"write2 ...{i}")
        time.sleep(0.1)
    lk.release()

if __name__ == '__main__':
    lk = multiprocessing.Lock()  # 创建锁
    w1 = multiprocessing.Process(target=write1, args=(lk,))
    w2 = multiprocessing.Process(target=write2, args=(lk,))

    w1.start()
    w2.start()
    w1.join()
    w2.join()

‘’’
write1 ...0
write1 ...1
write1 ...2
write1 ...3
write1 ...4
write2 ...0
write2 ...1
write2 ...2
write2 ...3
write2 ...4
’‘’

RLock

class multiprocessing.RLock

递归锁对象: 类似于 threading.RLock 。递归锁必须由持有线程、进程亲自释放。如果某个进程或者线程拿到了递归锁,这个进程或者线程可以再次拿到这个锁而不需要等待。但是这个进程或者线程的拿锁操作和释放锁操作的次数必须相同。
注意 RLock 是一个工厂函数,调用后返回一个使用默认 context 初始化的 multiprocessing.synchronize.RLock 实例。
RLock 支持 context manager 协议,因此可在 with 语句内使用。

主要方法

acquire(block=True, timeout=None)
可以阻塞或非阻塞地获得锁。

  • 当 block 参数设置为 True 时,会一直阻塞直到锁处于空闲状态(没有被任何进程、线程拥有),除非当前进程或线程已经拥有了这把锁。然后当前进程/线程会持有这把锁(在锁没有其他持有者的情况下),锁内的递归等级加一,并返回 True . 注意, 这个函数第一个参数的行为和 threading.RLock.acquire() 的实现有几个不同点,包括参数名本身。
  • 当 block 参数是 False , 将不会阻塞,如果此时锁被其他进程或者线程持有,当前进程、线程获取锁操作失败,锁的递归等级也不会改变,函数返回 False , 如果当前锁已经处于释放状态,则当前进程、线程则会拿到锁,并且锁内的递归等级加一,函数返回 True 。
  • timeout 参数的使用方法及行为与 Lock.acquire() 一样。但是要注意 timeout 的其中一些行为和 threading.RLock.acquire() 中实现的行为是不同的。

release()
释放锁,使锁内的递归等级减一。如果释放后锁内的递归等级降低为0,则会重置锁的状态为释放状态(即没有被任何进程、线程持有),重置后如果有有其他进程和线程在等待这把锁,他们中的一个会获得这个锁而继续运行。如果释放后锁内的递归等级还没到达0,则这个锁仍将保持未释放状态且当前进程和线程仍然是持有者。
只有当前进程或线程是锁的持有者时,才允许调用这个方法。如果当前进程或线程不是这个锁的拥有者,或者这个锁处于已释放的状态(即没有任何拥有者),调用这个方法会抛出 AssertionError 异常。注意这里抛出的异常类型和 threading.RLock.release() 中实现的行为不一样。

import os, time
import multiprocessing



def write1(lk):
    with lk:
        for i in range(5):
            print(f"write1 ...{i}")
            time.sleep(0.1)

def write2(lk):
    with lk :
        for i in range(5):
            print(f"write2 ...{i}")
            time.sleep(0.1)

if __name__ == '__main__':
    lk = multiprocessing.RLock()  # 创建锁
    w1 = multiprocessing.Process(target=write1, args=(lk,))
    w2 = multiprocessing.Process(target=write2, args=(lk,))

    w1.start()
    w2.start()
    w1.join()
    w2.join()

‘’'
write1 ...0
write1 ...1
write1 ...2
write1 ...3
write1 ...4
write2 ...0
write2 ...1
write2 ...2
write2 ...3
write2 ...4
‘''

实践发现Lock也是支持with操作的

Semaphore

一种信号量对象: 类似于 threading.Semaphore.
一个小小的不同在于,它的 acquire  方法的第一个参数名是和 Lock.acquire() 一样的 block 。
备注 在 macOS 上,不支持 sem_timedwait ,所以,调用 acquire() 时如果使用 timeout 参数,会通过循环sleep来模拟这个函数的行为。

class multiprocessing.Semaphore([value])

该类实现信号量对象。信号量对象管理一个原子性的计数器,代表 release() 方法的调用次数减去 acquire() 的调用次数再加上一个初始值。如果需要, acquire() 方法将会阻塞直到可以返回而不会使得计数器变成负数。在没有显式给出 value 的值时,默认为1。
可选参数 value 赋予内部计数器初始值,默认值为 1 。如果 value 被赋予小于0的值,将会引发 ValueError 异常。

备注 假如信号 SIGINT 是来自于 Ctrl-C ,并且主线程被 BoundedSemaphore.acquire(), Lock.acquire(), RLock.acquire(), Semaphore.acquire(), Condition.acquire() 或 Condition.wait() 阻塞,则调用会立即中断同时抛出 KeyboardInterrupt 异常。
这和 threading 的行为不同,此模块中当执行对应的阻塞式调用时,SIGINT 会被忽略。
备注 这个包的某些功能依赖于宿主机系统的共享信号量的实现,如果系统没有这个特性, multiprocessing.synchronize 会被禁用,尝试导入这个模块会引发 ImportError 异常,详细信息请查看 bpo-3770 。

主要方法:

acquire(block=True, timeout=None)
获取一个信号量。
在不带参数的情况下调用时:
如果在进入时内部计数器的值大于零,则将其减一并立即返回 True。
如果在进入时内部计数器的值为零,则将会阻塞直到被对 release() 的调用唤醒。 一旦被唤醒(并且计数器的值大于 0),则将计数器减 1 并返回 True。 每次对 release() 的调用将只唤醒一个线程。 线程被唤醒的次序是不可确定的。
当 block 设置为 False 时调用,不会阻塞。 如果没有参数的调用会阻塞,立即返回 False;否则,做与无参数调用相同的事情时返回 True。
当发起调用时如果 timeout 不为 None,则它将阻塞最多 timeout 秒。 请求在此时段时未能成功完成获取则将返回 False。 在其他情况下返回 True。


release(n=1)
释放一个信号量,将内部计数器的值增加 n。 当进入时值为零且有其他进程正在等待它再次变为大于零时,则唤醒那 n 个进程。
 

import os, time,sys
import multiprocessing
import multiprocessing.connection



def adder(sem : multiprocessing.Semaphore, conn : multiprocessing.connection.Connection):
    print(f'addworker ...')
    for i in range(5):
        conn.send('aaa' + str(i))
        sem.release()
    # time.sleep(5)

def worker(sem: multiprocessing.Semaphore, conn: multiprocessing.connection.Connection):
    print(f'worker{os.getpid()}...')
    while sem.acquire(block=True, timeout=3):
        print(f'worker{os.getpid()}...begin recv()')
        if conn.poll(timeout=3) :
            data = conn.recv()
            print(f'worker{os.getpid()}, {data=}')
        else:
            print(f'worker{os.getpid()} timeout')
            break
        time.sleep(0.5)


if __name__ == '__main__':
    print(sys.version_info)
    sem = multiprocessing.Semaphore(0)
    conn1,conn2 = multiprocessing.Pipe()
    # adder(sem, conn1)
    p0 = multiprocessing.Process(target=adder, args=(sem, conn1))
    p0.start()

    p1 = multiprocessing.Process(target=worker, args=(sem, conn2))
    p1.start()
    p0.join()
    p1.join()

‘’'
sys.version_info(major=3, minor=11, micro=4, releaselevel='final', serial=0)
addworker ...
worker1365...
worker1365...begin recv()
worker1365, data='aaa0'
worker1365...begin recv()
worker1365, data='aaa1'
worker1365...begin recv()
worker1365, data='aaa2'
worker1365...begin recv()
worker1365, data='aaa3'
worker1365...begin recv()
worker1365, data='aaa4'

‘''

BoundedSemaphore

class multiprocessing.BoundedSemaphore([value])

该类实现有界信号量。有界信号量通过检查以确保它当前的值不会超过初始值。如果超过了初始值,将会引发 ValueError 异常。在大多情况下,信号量用于保护数量有限的资源。如果信号量被释放的次数过多,则表明出现了错误。没有指定时, value 的值默认为1。

Event

进程间的事件信号通信,一个进程发出事件信号,而其他进程等待该信号。一个事件对象管理一个内部标识,调用 set() 方法可将其设置为 true ,调用 clear() 方法可将其设置为 false ,调用 wait() 方法将进入阻塞直到标识为 true 。

与threading.Event的用法完全一致。

class multiprocessing.Event

主要方法:

is_set()
当且仅当内部标识为 true 时返回 True 。
isSet 方法是此方法的已弃用别名。

set()
将内部标识设置为 true 。所有正在等待这个事件的线程将被唤醒。当标识为 true 时,调用 wait() 方法的线程不会被被阻塞。

clear()
将内部标识设置为 false 。之后调用 wait() 方法的线程将会被阻塞,直到调用 set() 方法将内部标识再次设置为 true 。

wait(timeout=None)
阻塞线程直到内部变量为 true 。如果调用时内部标识为 true,将立即返回。否则将阻塞线程,直到调用 set() 方法将标识设置为 true 或者发生可选的超时。
当提供了timeout参数且不是 None 时,它应该是一个浮点数,代表操作的超时时间,以秒为单位(可以为小数)。
当且仅当内部旗标在等待调用之前或者等待开始之后被设为真值时此方法将返回 True,也就是说,它将总是返回 True 除非设定了超时且操作发生了超时。
在 3.1 版更改: 很明显,方法总是返回 None。

import os, time,sys
import multiprocessing
import multiprocessing.connection



def adder(ev : multiprocessing.Event, conn : multiprocessing.connection.Connection):
    print(f'addworker ...')
    for i in range(5):
        print(f'ev.set()')
        conn.send('hello:' + str(i))
        ev.set()
        time.sleep(1)

def worker(ev: multiprocessing.Event, conn: multiprocessing.connection.Connection):
    print(f'worker{os.getpid()}...')
    while ev.wait(timeout=3):
        print(f'worker{os.getpid()}...begin recv()')
        data = conn.recv()
        print(f'worker{os.getpid()}, {data=}')
        ev.clear()


if __name__ == '__main__':
    print(sys.version_info)
    ev = multiprocessing.Event()
    ev.clear()
    conn1,conn2 = multiprocessing.Pipe()
    p0 = multiprocessing.Process(target=adder, args=(ev, conn1))
    p0.start()

    p1 = multiprocessing.Process(target=worker, args=(ev, conn2))
    p1.start()
    p0.join()
    p1.join()

‘’'
sys.version_info(major=3, minor=11, micro=4, releaselevel='final', serial=0)
addworker ...
ev.set()
worker2061...
worker2061...begin recv()
worker2061, data='hello:0'
ev.set()
worker2061...begin recv()
worker2061, data='hello:1'
ev.set()
worker2061...begin recv()
worker2061, data='hello:2'
ev.set()
worker2061...begin recv()
worker2061, data='hello:3'
ev.set()
worker2061...begin recv()
worker2061, data='hello:4'
‘''

Condition

class multiprocessing.Condition([lock])

实际是threading.Condition 的别名。

实现条件变量对象的类。一个条件变量对象允许一个或多个进程在被其它进程所通知之前进行等待。
如果给出了非 None 的 lock 参数,则它必须为 Lock 或者 RLock 对象,并且它将被用作底层锁。否则,将会创建新的 RLock 对象,并将其用作底层锁。
在 3.3 版更改: 从工厂函数变为类。

主要方法:

acquire(*args)
请求底层锁。此方法调用底层锁的相应方法,返回值是底层锁相应方法的返回值。

release()
释放底层锁。此方法调用底层锁的相应方法。没有返回值。

wait(timeout=None)
等待直到被通知或发生超时。如果线程在调用此方法时没有获得锁,将会引发 RuntimeError 异常。
这个方法释放底层锁,然后阻塞,直到在另外一个线程中调用同一个条件变量的 notify() 或 notify_all() 唤醒它,或者直到可选的超时发生。一旦被唤醒或者超时,它重新获得锁并返回。
当提供了 timeout 参数且不是 None 时,它应该是一个浮点数,代表操作的超时时间,以秒为单位(可以为小数)。
当底层锁是个 RLock ,不会使用它的 release() 方法释放锁,因为当它被递归多次获取时,实际上可能无法解锁。相反,使用了 RLock 类的内部接口,即使多次递归获取它也能解锁它。 然后,在重新获取锁时,使用另一个内部接口来恢复递归级别。
返回 True ,除非提供的 timeout 过期,这种情况下返回 False。
在 3.2 版更改: 很明显,方法总是返回 None。

wait_for(predicate, timeout=None)
等待,直到条件计算为真。 predicate 应该是一个可调用对象而且它的返回值可被解释为一个布尔值。可以提供 timeout 参数给出最大等待时间。
这个实用方法会重复地调用 wait() 直到满足判断式或者发生超时。返回值是判断式最后一个返回值,而且如果方法发生超时会返回 False 。

notify(n=1)
默认唤醒一个等待这个条件的进程。如果调用进程在没有获得锁的情况下调用这个方法,会引发 RuntimeError 异常。
这个方法唤醒最多 n 个正在等待这个条件变量的进程;如果没有进程在等待,这是一个空操作。
当前实现中,如果至少有 n 个进程正在等待,准确唤醒 n 个进程。但是依赖这个行为并不安全。未来,优化的实现有时会唤醒超过 n 个进程。
注意:被唤醒的进程并没有真正恢复到它调用的 wait() ,直到它可以重新获得锁。 因为 notify() 不释放锁,其调用者才应该这样做。

notify_all()
唤醒所有正在等待这个条件的进程。这个方法行为与 notify() 相似,但并不只唤醒单一进程,而是唤醒所有等待进程。如果调用线程在调用这个方法时没有获得锁,会引发 RuntimeError 异常。
notifyAll 方法是此方法的已弃用别名。

Barrier

class multiprocessing.Barrier(parties[, action[, timeout]])

创建一个需要 parties 个进程的栅栏对象。如果提供了可调用的 action 参数,它会在所有进程被释放时在其中一个进程中自动调用。 timeout 是默认的超时时间,如果没有在 wait() 方法中指定超时时间的话。

主要方法和属性:

wait(timeout=None)
冲出栅栏。当栅栏中所有进程都已经调用了这个函数,它们将同时被释放。如果提供了 timeout 参数,这里的 timeout 参数优先于创建栅栏对象时提供的 timeout 参数。
函数返回值是一个整数,取值范围在0到 parties -- 1,在每个进程中的返回值不相同。可用于从所有进程中选择唯一的一个进程执行一些特别的工作。
如果创建栅栏对象时在构造函数中提供了 action 参数,它将在其中一个进程释放前被调用。如果此调用引发了异常,栅栏对象将进入损坏态。
如果发生了超时,栅栏对象将进入破损态。
如果栅栏对象进入破损态,或重置栅栏时仍有线程等待释放,将会引发 BrokenBarrierError 异常。

reset()
重置栅栏为默认的初始态。如果栅栏中仍有进程等待释放,这些进程程将会收到 BrokenBarrierError 异常。
请注意使用此函数时,如果存在状态未知的其他进程,则可能需要执行外部同步。 如果栅栏已损坏则最好将其废弃并新建一个。

abort()
使栅栏处于损坏状态。 这将导致任何现有和未来对 wait() 的调用失败并引发 BrokenBarrierError。 例如可以在需要中止某个进程时使用此方法,以避免应用程序的死锁。
更好的方式是:创建栅栏时提供一个合理的超时时间,来自动避免某个进程出错。

parties
冲出栅栏所需要的进程数量。

n_waiting
当前时刻正在栅栏中阻塞的进程数量。

broken
一个布尔值,值为 True 表明栅栏为破损态。

管理器

Python实现多进程间通信的方式有很多种,例如队列,管道等。但是这些方式只适用于多个进程都是源于同一个父进程的情况。如果多个进程不是源于同一个父进程,只能用共享内存,信号量等方式,但是这些方式对于复杂的数据结构,例如Queue,dict,list等,使用起来比较麻烦,不够灵活。

Manager是一种较为高级的多进程通信方式,它能支持任意进程间通讯,也可以在进程池中使用。

它的原理是:先启动一个ManagerServer进程,这个进程是阻塞的,它监听一个socket,然后其他进程(ManagerClient)通过socket来连接到ManagerServer,实现通信。Manager实现的共享机制比较复杂,multiprocessing对其进行了一些封装。

multiprocessing.Manager()

返回一个已启动的 SyncManager 管理器对象,这个对象可以用于在不同进程中共享数据。返回的管理器对象对应了一个已经启动的子进程,并且拥有一系列方法可以用于创建共享对象、返回对应的代理。

from multiprocessing import Process, Manager, Pool

def addItem(item, listshared):
    listshared.append(item)


if __name__ == "__main__":
    manager = Manager()
    sharedList = manager.list()

    p1 = Process(target=addItem, args=("apple", sharedList))
    p2 = Process(target=addItem, args=("banana", sharedList))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    print(sharedList)

    pool = Pool(2)
    pool.apply_async(addItem, ('John', sharedList))
    pool.apply_async(addItem, ('John', sharedList))
    pool.close()
    pool.join()
    print(sharedList)

‘’'
['apple', 'banana']
['apple', 'banana', 'John', 'John’]
‘''

管理器非常强大,可以在子进程与父进程之间、子进程之间实现变量的共享,也可以适用于进程池。

SyncManager

Manager()实际返回的是SyncManager对象,SyncManager是实际用于进程同步的对象。
它拥有一系列方法,可以为大部分常用数据类型创建并返回 代理对象 代理,用于进程间同步。甚至包括共享列表和字典。

主要方法和属性

属性和方法名说明
Barrier(parties[, action[, timeout]])创建一个共享的 threading.Barrier 对象并返回它的代理。
BoundedSemaphore([value])创建一个共享的 threading.BoundedSemaphore 对象并返回它的代理。
Condition([lock])创建一个共享的 threading.Condition 对象并返回它的代理。
如果提供了 lock 参数,那它必须是 threading.Lock 或 threading.RLock 的代理对象。
Event()创建一个共享的 threading.Event 对象并返回它的代理。
Lock()创建一个共享的 threading.Lock 对象并返回它的代理。
Namespace()创建一个共享的 Namespace 对象并返回它的代理。
Queue([maxsize])创建一个共享的 queue.Queue 对象并返回它的代理。
RLock()创建一个共享的 threading.RLock 对象并返回它的代理。
Semaphore([value])创建一个共享的 threading.Semaphore 对象并返回它的代理。
Array(typecode, sequence)创建一个数组并返回它的代理。
Value(typecode, value)创建一个具有可写 value 属性的对象并返回它的代理。
dict()
dict(mapping)
dict(sequence)
创建一个共享的 dict 对象并返回它的代理。
list()
list(sequence)
创建一个共享的 list 对象并返回它的代理。

共享字典

from multiprocessing import Process, Manager

def add_item(key, value, shared_dict):
    shared_dict[key] = value

if __name__ == "__main__":
    manager = Manager()
    shared_dict = manager.dict()

    p1 = Process(target=add_item, args=("apple", 1, shared_dict))
    p2 = Process(target=add_item, args=("banana", 2, shared_dict))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    print(shared_dict)

在这个示例中,我们使用multiprocessing.Manager类来共享一个字典。我们首先创建了一个Manager对象,然后使用它来创建一个共享字典。接着,我们创建了两个进程,每个进程都调用add_item函数来向共享字典中添加一个键值对。最后,打印了更新后的共享字典。

BaseManager对象

SyncManager很强大,但是如果要共享自定义的类,使用起来就会比较复杂。而实际上SyncManager是BaseManager的子类,我们也可以自己定义BaseManager的子类实现自己的类型的共享。

class multiprocessing.managers.BaseManager(address=Noneauthkey=Noneserializer='pickle'ctx=None*shutdown_timeout=1.0)

创建一个 BaseManager 对象。

  • 一旦创建,应该及时调用 start() 或者 get_server().serve_forever() 以确保管理器对象对应的管理进程已经启动。
  • address 是管理器服务进程监听的地址。如果 address 是 None ,则允许和任意主机的请求建立连接。
  • authkey 是认证标识,用于检查连接服务进程的请求合法性。如果 authkey 是 None, 则会使用 current_process().authkey , 否则,就使用 authkey , 需要保证它必须是 byte 类型的字符串。
  • serializer 必须为 'pickle' (使用 pickle 序列化) 或 'xmlrpclib' (使用 xmlrpc.client 序列化)。
  • ctx 是一个上下文对象,或者为 None (使用当前上下文)。 参见 get_context() 函数。
  • shutdown_timeout 是用于等待直到 shutdown() 方法中的管理器所使用的进程结束的超时秒数。 如果关闭超时,进程将被终结。 如果终结进程的操作也超时,进程将被杀掉。

主要方法和属性

方法和属性名说明
address管理器所用的IP地址
start([initializer[, initargs]])为管理器开启一个子进程,如果 initializer 不是 None , 子进程在启动时将会调用 initializer(*initargs)
get_server()返回一个 Server  对象,它是管理器在后台控制的真实的服务。 Server  对象拥有 serve_forever() 方法。
connect()将本地管理器对象连接到一个远程管理器进程
shutdown()停止管理器的进程。这个方法只能用于已经使用 start() 启动的服务进程。
它可以被多次调用。
register(typeid[, callable[, proxytype[, exposed[, method_to_typeid[, create_method]]]]])一个 classmethod,可以将一个类型或者可调用对象注册到管理器类。
typeid 是一种 "类型标识符",用于唯一表示某种共享对象类型,必须是一个字符串。
callable 是一个用来为此类型标识符创建对象的可调用对象。如果一个管理器实例将使用 connect() 方法连接到服务器,或者 create_method 参数为 False,那么这里可留下 None。
proxytype 是 BaseProxy  的子类,可以根据 typeid 为共享对象创建一个代理,如果是 None , 则会自动创建一个代理类。
exposed 是一个函数名组成的序列,用来指明只有这些方法可以使用 BaseProxy._callmethod() 代理。(如果 exposed 是 None, 则会在 proxytype._exposed_ 存在的情况下转而使用它) 当暴露的方法列表没有指定的时候,共享对象的所有 “公共方法” 都会被代理。(这里的“公共方法”是指所有拥有 __call__() 方法并且不是以 '_' 开头的属性)
method_to_typeid 是一个映射,用来指定那些应该返回代理对象的暴露方法所返回的类型。(如果 method_to_typeid 是 None, 则 proxytype._method_to_typeid_ 会在存在的情况下被使用)如果方法名称不在这个映射中或者映射是 None ,则方法返回的对象会是一个值拷贝。
create_method 指明,是否要创建一个以 typeid 命名并返回一个代理对象的方法,这个函数会被服务进程用于创建共享对象,默认为 True 。
from multiprocessing.managers import BaseManager
from multiprocessing import Process
import os,sys,time

class MyClass():
    def __init__(self):
        self.numlist = []

    def append(self, a):
        self.numlist.append(a)
        print(f'进程{os.getpid()} append, {self.numlist=}')

    def pop(self):
        print(f'进程{os.getpid()} pop, {self.numlist=}')
        if len(self.numlist) > 0:
            return self.numlist.pop()
        else:
            time.sleep(1)
            return self.numlist.pop()

def getMyClass():
    return MyClass()

def myappend(co):
    print(f'进程{os.getpid()} myappend...')
    for i in range(10):
        co.append(i)

def mypop(co):
    for i in range(10):
        num = co.pop()
        print(f'进程{os.getpid()},mypop:{num=}')

if __name__ == '__main__':
    print(f'主进程{os.getpid()}...')
    manager = BaseManager()
    manager.register('getMyClass', MyClass)
    manager.start()
    co = manager.getMyClass()
    print(co)
    co.append(100)

    p1 = Process(target=myappend, args=(co,))
    p1.start()
    p2 = Process(target=mypop, args=(co,))
    p2.start()
    p1.join()
    p2.join()

‘’'
主进程8565...
<__mp_main__.MyClass object at 0x102fda710>
进程8567 append, self.numlist=[100]
进程8569 myappend...
进程8567 append, self.numlist=[100, 0]
进程8567 append, self.numlist=[100, 0, 1]
进程8567 append, self.numlist=[100, 0, 1, 2]
进程8567 append, self.numlist=[100, 0, 1, 2, 3]
进程8567 append, self.numlist=[100, 0, 1, 2, 3, 4]
进程8567 append, self.numlist=[100, 0, 1, 2, 3, 4, 5]
进程8567 append, self.numlist=[100, 0, 1, 2, 3, 4, 5, 6]
进程8567 append, self.numlist=[100, 0, 1, 2, 3, 4, 5, 6, 7]
进程8567 append, self.numlist=[100, 0, 1, 2, 3, 4, 5, 6, 7, 8]
进程8567 append, self.numlist=[100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
进程8567 pop, self.numlist=[100, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
进程8570,mypop:num=9
进程8567 pop, self.numlist=[100, 0, 1, 2, 3, 4, 5, 6, 7, 8]
进程8570,mypop:num=8
进程8567 pop, self.numlist=[100, 0, 1, 2, 3, 4, 5, 6, 7]
进程8570,mypop:num=7
进程8567 pop, self.numlist=[100, 0, 1, 2, 3, 4, 5, 6]
进程8570,mypop:num=6
进程8567 pop, self.numlist=[100, 0, 1, 2, 3, 4, 5]
进程8570,mypop:num=5
进程8567 pop, self.numlist=[100, 0, 1, 2, 3, 4]
进程8570,mypop:num=4
进程8567 pop, self.numlist=[100, 0, 1, 2, 3]
进程8570,mypop:num=3
进程8567 pop, self.numlist=[100, 0, 1, 2]
进程8570,mypop:num=2
进程8567 pop, self.numlist=[100, 0, 1]
进程8570,mypop:num=1
进程8567 pop, self.numlist=[100, 0]
进程8570,mypop:num=0
‘''

从上面的例子可以看到:

  • BaseManager实际是单独启动了一个独立的进程8567,主进程是8565,myappend子进程是8569,mypop子进程是8570
  • MyClass的实际操作都是在BaseManager进程(8567)上完成的。
  • 无论是主进程,还是子进程,都只是通过socket接口调用了8567上的服务。

共享内存

multiprocessing.shared_memory是python3.8提供的新功能。

该模块提供了一个 SharedMemory 类,用于分配和管理多核或对称多处理器(SMP)机器上进程间的共享内存。为了协助管理不同进程间的共享内存生命周期,multiprocessing.managers 模块也提供了一个 BaseManager 的子类: SharedMemoryManager。
本模块中,共享内存是指 "System V 类型" 的共享内存块(虽然可能和它实现方式不完全一致)而不是 “分布式共享内存”。这种类型的的共享内存允许不同进程读写一片公共(或者共享)的易失性存储区域。一般来说,进程被限制只能访问属于自己进程空间的内存,但是共享内存允许跨进程共享数据,从而避免通过进程间发送消息的形式传递数据。相比通过磁盘、套接字或者其他要求序列化、反序列化和复制数据的共享形式,直接通过内存共享数据拥有更出色性能。

class multiprocessing.shared_memory.SharedMemory(name=Nonecreate=Falsesize=0)

  • 创建一个新的共享内存块或者连接到一片已经存在的共享内存块。每个共享内存块都被指定了一个全局唯一的名称。通过这种方式,进程可以使用一个特定的名字创建共享内存区块,然后其他进程使用同样的名字连接到这个共享内存块。
  • 作为一种跨进程共享数据的方式,共享内存块的寿命可能超过创建它的原始进程。一个共享内存块可能同时被多个进程使用,当一个进程不再需要访问这个共享内存块的时候,应该调用 close() 方法。当一个共享内存块不被任何进程使用的时候,应该调用 unlink() 方法以执行必要的清理。
  • name 是共享内存的唯一名称,字符串类型。如果创建一个新共享内存块的时候,名称指定为 None (默认值),将会随机产生一个新名称。
  • create 指定创建一个新的共享内存块 (True) 还是连接到已存在的共享内存块 (False) 。
  • 如果是新创建共享内存块则 size 用于指定块的大小为多少字节。由于某些平台是以内存页大小为最小单位来分配内存的,最终得到的内存块大小可能大于或等于要求的大小。如果是连接到已经存在的共享内存块, size 参数会被忽略。

主要方法和属性

方法和属性名说明
close()关闭实例对于共享内存的访问连接。所有实例确认自己不再需要使用共享内存的时候都应该调用 close() ,以保证必要的资源清理。调用 close() 并不会销毁共享内存区域。
unlink()请求销毁底层的共享内存块。 为了执行必要的资源清理,在所有使用这个共享内存块的进程中,unlink() 应该调用一次(且只能调用一次)。 发出此销毁请求后,共享内存块可能会、也可能不会立即销毁,且此行为在不同操作系统之间可能不同。 调用 unlink() 后再尝试访问其中的数据可能导致内存错误。 注意:最后一个关闭共享内存访问权限的进程可以以任意顺序调用 unlink() 和 close()。
buf共享内存块内容的 memoryview
name共享内存块的唯一标识,只读属性。
size共享内存块的字节大小,只读属性。
>>>from multiprocessing import shared_memory
>>>shm_a = shared_memory.SharedMemory(create=True, size=10)
>>>type(shm_a.buf)
<class 'memoryview'>
>>>buffer = shm_a.buf
>>>len(buffer)
10
>>>buffer[:4] = bytearray([22, 33, 44, 55])  # Modify multiple at once
>>>buffer[4] = 100                           # Modify single byte at a time
# Attach to an existing shared memory block
>>>shm_b = shared_memory.SharedMemory(shm_a.name)
>>>import array
>>>array.array('b', shm_b.buf[:5])  # Copy the data into a new array.array
array('b', [22, 33, 44, 55, 100])
>>>shm_b.buf[:5] = b'howdy'  # Modify via shm_b using bytes
>>>bytes(shm_a.buf[:5])      # Access via shm_a
b'howdy'
>>>shm_b.close()   # Close each SharedMemory instance
>>>shm_a.close()
>>>shm_a.unlink()  # Cal

SharedMemoryManager

class multiprocessing.managers.SharedMemoryManager([address[, authkey]])

BaseManager 的子类,可用于管理跨进程的共享内存块。
调用 SharedMemoryManager 实例上的 start() 方法会启动一个新进程。这个新进程的唯一目的就是管理所有由它创建的共享内存块的生命周期。想要释放此进程管理的所有共享内存块,可以调用实例的 shutdown() 方法。这会触发执行它管理的所有 SharedMemory 对象的 SharedMemory.unlink() 方法,然后停止这个进程。通过 SharedMemoryManager 创建 SharedMemory 实例,我们可以避免手动跟踪和释放共享内存资源。
这个类提供了创建和返回 SharedMemory 实例的方法,以及以共享内存为基础创建一个列表类对象 (ShareableList) 的方法。

SharedMemory(size)
使用 size 参数,创建一个新的指定字节大小的 SharedMemory 对象并返回。

ShareableList(sequence)
创建并返回一个新的 ShareableList 对象,通过输入参数 sequence 初始化。

>>>from multiprocessing.managers import SharedMemoryManager
>>>smm = SharedMemoryManager()
>>>smm.start()  # Start the process that manages the shared memory blocks
>>>sl = smm.ShareableList(range(4))
>>>sl
ShareableList([0, 1, 2, 3], name='psm_6572_7512')
>>>raw_shm = smm.SharedMemory(size=128)
>>>another_sl = smm.ShareableList('alpha')
>>>another_sl
ShareableList(['a', 'l', 'p', 'h', 'a'], name='psm_6572_12221')
>>>smm.shutdown()  # Calls unlink() on sl, raw_shm, and another_sl

以下案例展示了 SharedMemoryManager 对象的一种可能更方便的使用方式,通过 with 语句来保证所有共享内存块在使用完后被释放。

>>>with SharedMemoryManager() as smm:
    sl = smm.ShareableList(range(2000))
    # Divide the work among two processes, storing partial results in sl
    p1 = Process(target=do_work, args=(sl, 0, 1000))
    p2 = Process(target=do_work, args=(sl, 1000, 2000))
    p1.start()
    p2.start()  # A multiprocessing.Pool might be more efficient
    p1.join()
    p2.join()   # Wait for all work to complete in both processes
    total_result = sum(sl)  # Consolidate the partial results now in sl

在 with 语句中使用 SharedMemoryManager  对象的时候,使用这个管理器创建的共享内存块会在 with 语句代码块结束后被释放。

ShareableList

class multiprocessing.shared_memory.ShareableList(sequence=None, \*, name=None)
提供了一个类似于可变列表的对象,其中存储的所有值都存储在一个共享内存块中。 这限制了可存储的值只能是 int (带符号的 64 位), float, bool, str (当以 utf-8 编码时每个值小于 10M 字节), bytes (每个值小于 10M 字节) 和 None 内置数据类型。 它与 list 内置类型的另一个显著区别在于这些列表不能改变其总长度 (例如不能追加、插入等),也不支持通过切片动态创建新的 ShareableList 实例。

sequence 会被用来为一个新的 ShareableList 填充值。 设为 None 则会基于唯一的共享内存名称关联到已经存在的 ShareableList。

name 是所请求的共享内存的唯一名称,与 SharedMemory 的定义中所描述的一致。 当关联到现有的 ShareableList 时,则指明其共享内存块的唯一名称并将 sequence 设为 None。

主要属性和方法

属性和方法名说明
count(value)返回 value 出现的次数。
index(value)返回 value 首次出现的位置,如果 value 不存在, 则抛出 ValueError 异常。
format包含由所有当前存储值所使用的 struct 打包格式的只读属性。
shm存储了值的 SharedMemory 实例。
>>>from multiprocessing import shared_memory
>>>a = shared_memory.ShareableList(['howdy', b'HoWdY', -273.154, 100, None, True, 42])
>>>[ type(entry) for entry in a ]
[<class 'str'>, <class 'bytes'>, <class 'float'>, <class 'int'>, <class 'NoneType'>, <class 'bool'>, <class 'int'>]
>>>a[2]
-273.154
>>>a[2] = -78.5
>>>a[2]
-78.5
>>>a[2] = 'dry ice'  # Changing data types is supported as well
>>>a[2]
'dry ice'
>>>a[2] = 'larger than previously allocated storage space'
Traceback (most recent call last):
  ...
ValueError: exceeds available storage for existing str
>>>a[2]
'dry ice'
>>>len(a)
7
>>>a.index(42)
6
>>>a.count(b'howdy')
0
>>>a.count(b'HoWdY')
1
>>>a.shm.close()
>>>a.shm.unlink()
>>>del a  # Use of a ShareableList after call to unlink() is unsupported
>>>b = shared_memory.ShareableList(range(5))         # In a first process
>>>c = shared_memory.ShareableList(name=b.shm.name)  # In a second process
>>>c
ShareableList([0, 1, 2, 3, 4], name='...')
>>>c[-1] = -999
>>>b[-1]
-999
>>>b.shm.close()
>>>c.shm.close()
>>>c.shm.unlink()

下面的例子显示 ShareableList (以及下层的 SharedMemory) 对象可以在必要时被封存和解封。 请注意,它将仍然为同一个共享对象。 出现这种情况是因为被反序列化的对象具有相同的唯一名称并使用这个相同的名称附加到现有的对象上(如果对象仍然保持存活):

>>>import pickle
>>>from multiprocessing import shared_memory
>>>sl = shared_memory.ShareableList(range(10))
>>>list(sl)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>>deserialized_sl = pickle.loads(pickle.dumps(sl))
>>>list(deserialized_sl)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>>sl[0] = -1
>>>deserialized_sl[1] = -2
>>>list(sl)
[-1, -2, 2, 3, 4, 5, 6, 7, 8, 9]
>>>list(deserialized_sl)
[-1, -2, 2, 3, 4, 5, 6, 7, 8, 9]
>>>sl.shm.close()
>>>sl.shm.unlink()

共享ctype对象

ctypes 是 Python 的外部函数库。它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的函数。可使用该模块以纯 Python 形式对这些库进行封装。

在共享内存上创建可被子进程继承的共享对象时是可行的,不能使用在进程池中。

Value

multiprocessing.Value(typecode_or_type*argslock=True)

返回一个从共享内存上创建的 ctypes 对象。默认情况下返回的对象实际上是经过了同步器包装过的。可以通过 Value 的 value 属性访问这个对象本身。

  • typecode_or_type 指明了返回的对象类型: 它可能是一个 ctypes 类型或者 array  模块中每个类型对应的单字符长度的字符串,参见Array。 *args 会透传给这个类的构造函数。
  • 如果 lock 参数是 True (默认值), 将会新建一个递归锁用于同步对于此值的访问操作。 如果 lock 是 Lock 或者 RLock 对象,那么这个传入的锁将会用于同步对这个值的访问操作,如果 lock 是 False , 那么对这个对象的访问将没有锁保护,也就是说这个变量不是进程安全的。

诸如 += 这类的操作会引发独立的读操作和写操作,也就是说这类操作符并不具有原子性。所以,如果你想让递增共享变量的操作具有原子性,仅仅以这样的方式并不能达到要求:

counter.value += 1
共享对象内部关联的锁是递归锁(默认情况下就是)的情况下, 你可以采用这种方式
with counter.get_lock():
    counter.value += 1
注意 lock 只能是命名参数。

import multiprocessing

def foo(n, a):
    n.value = 3.14
    a[0] = 5

if __name__ == '__main__':
    num = multiprocessing.Value('d', 0.0) #d 表示double
    print(type(num)) #<class 'multiprocessing.sharedctypes.Synchronized'>
    arr = multiprocessing.Array('i', range(10))
    print(type(arr)) #<class 'multiprocessing.sharedctypes.SynchronizedArray'>
    print(dir(arr))
    arr[0] = 100
    print(arr[:])
    p1 = multiprocessing.Process(target=foo, args=(num, arr))
    p1.start()
    p1.join()
    print(num.value)
    print(arr[:])

‘’'
<class 'multiprocessing.sharedctypes.Synchronized'>
<class 'multiprocessing.sharedctypes.SynchronizedArray'>
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__enter__', '__eq__', '__exit__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getslice__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__setslice__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_lock', '_obj', 'acquire', 'get_lock', 'get_obj', 'release']
[100, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3.14
[5, 1, 2, 3, 4, 5, 6, 7, 8, 9]
‘''

Array

multiprocessing.Array(typecode_or_typesize_or_initializer*lock=True)

从共享内存中申请并返回一个具有ctypes类型的数组对象。默认情况下返回值实际上是被同步器包装过的数组对象。
typecode_or_type 指明了返回的数组中的元素类型: 它可能是一个 ctypes 类型或者 array 模块中每个类型对应的单字符长度的字符串。 如果 size_or_initializer 是一个整数,那就会当做数组的长度,并且整个数组的内存会初始化为0。否则,如果 size_or_initializer 会被当成一个序列用于初始化数组中的每一个元素,并且会根据元素个数自动判断数组的长度。
如果 lock 为 True (默认值) 则将创建一个新的锁对象用于同步对值的访问。 如果 lock 为一个 Lock 或 RLock 对象则该对象将被用于同步对值的访问。 如果 lock 为 False 则对返回对象的访问将不会自动得到锁的保护,也就是说它不是“进程安全的”。
请注意 lock 是一个仅限关键字参数。
请注意 ctypes.c_char 的数组具有 value 和 raw 属性,允许被用来保存和提取字符串。

sharedctypes模块

multiprocessing.sharedctypes 模块提供了一些函数,用于分配来自共享内存的、可被子进程继承的 ctypes 对象。

虽然可以将指针存储在共享内存中,但请记住它所引用的是特定进程地址空间中的位置。 而且,指针很可能在第二个进程的上下文中无效,尝试从第二个进程对指针进行解引用可能会导致崩溃。

下面的表格对比了创建普通ctypes对象和基于共享内存上创建共享ctypes对象的语法。(表格中的 MyStruct 是 ctypes.Structure 的子类)

ctypes

使用类型的共享ctypes

使用 typecode 的共享 ctypes

c_double(2.4)

RawValue(c_double, 2.4)

RawValue('d', 2.4)

MyStruct(4, 6)

RawValue(MyStruct, 4, 6)

(c_short * 7)()

RawArray(c_short, 7)

RawArray('h', 7)

(c_int * 3)(9, 2, 8)

RawArray(c_int, (9, 2, 8))

RawArray('i', (9, 2, 8))

需要注意的是,访问包装后的ctypes对象会比直接访问原来的纯 ctypes 对象慢得多。

from multiprocessing import Process, Lock
from multiprocessing.sharedctypes import Value, Array
from ctypes import Structure, c_double

class Point(Structure):
    _fields_ = [('x', c_double), ('y', c_double)]

def modify(n, x, s, A):
    n.value **= 2
    x.value **= 2
    s.value = s.value.upper()
    for a in A:
        a.x **= 2
        a.y **= 2

if __name__ == '__main__':
    lock = Lock()

    n = Value('i', 7)
    x = Value(c_double, 1.0/3.0, lock=False)
    s = Array('c', b'hello world', lock=lock)
    A = Array(Point, [(1.875,-6.25), (-5.75,2.0), (2.375,9.5)], lock=lock)

    p = Process(target=modify, args=(n, x, s, A))
    p.start()
    p.join()

    print(n.value)
    print(x.value)
    print(s.value)
    print([(a.x, a.y) for a in A])

‘’'
49
0.1111111111111111
HELLO WORLD
[(3.515625, 39.0625), (33.0625, 4.0), (5.640625, 90.25)]
‘''

RawArray

multiprocessing.sharedctypes.RawArray(typecode_or_typesize_or_initializer)

从共享内存中申请并返回一个 ctypes 数组。

typecode_or_type 指明了返回的数组中的元素类型: 它可能是一个 ctypes 类型或者 array 模块中使用的类型字符。 如果 size_or_initializer 是一个整数,那就会当做数组的长度,并且整个数组的内存会初始化为0。否则,如果 size_or_initializer 会被当成一个序列用于初始化数组中的每一个元素,并且会根据元素个数自动判断数组的长度。
注意对元素的访问、赋值操作可能是非原子操作 - 使用 Array() , 从而借助其中的锁保证操作的原子性。

RawValue

multiprocessing.sharedctypes.RawValue(typecode_or_type, *args)
从共享内存中申请并返回一个 ctypes 对象。
typecode_or_type 指明了返回的对象类型: 它可能是一个 ctypes 类型或者 array  模块中每个类型对应的单字符长度的字符串。 *args 会透传给这个类的构造函数。
注意对 value 的访问、赋值操作可能是非原子操作 - 使用 Value() ,从而借助其中的锁保证操作的原子性。
请注意 ctypes.c_char 的数组具有 value 和 raw 属性,允许被用来保存和提取字符串 - 请查看 ctypes 文档。

Array

multiprocessing.sharedctypes.Array(typecode_or_type, size_or_initializer, *, lock=True)
返回一个纯 ctypes 数组, 或者在此之上经过同步器包装过的进程安全的对象,这取决于 lock 参数的值,除此之外,和 RawArray() 一样。
如果 lock 为 True (默认值) 则将创建一个新的锁对象用于同步对值的访问。 如果 lock 为一个 Lock 或 RLock 对象则该对象将被用于同步对值的访问。 如果 lock 为 False 则对所返回对象的访问将不会自动得到锁的保护,也就是说它将不是“进程安全的”。
注意 lock 只能是命名参数。

Value

multiprocessing.sharedctypes.Value(typecode_or_type, *args, lock=True)
返回一个纯 ctypes 数组, 或者在此之上经过同步器包装过的进程安全的对象,这取决于 lock 参数的值,除此之外,和 RawArray() 一样。
如果 lock 为 True (默认值) 则将创建一个新的锁对象用于同步对值的访问。 如果 lock 为一个 Lock 或 RLock 对象则该对象将被用于同步对值的访问。 如果 lock 为 False 则对所返回对象的访问将不会自动得到锁的保护,也就是说它将不是“进程安全的”。
注意 lock 只能是命名参数。

copy()

multiprocessing.sharedctypes.copy(obj)
从共享内存中申请一片空间将 ctypes 对象 obj 过来,然后返回一个新的 ctypes 对象。

synchronized()

multiprocessing.sharedctypes.synchronized(obj[, lock])
将一个 ctypes 对象包装为进程安全的对象并返回,使用 lock 同步对于它的操作。如果 lock 是 None (默认值) ,则会自动创建一个 multiprocessing.RLock 对象。
同步器包装后的对象会在原有对象基础上额外增加两个方法: get_obj() 返回被包装的对象, get_lock() 返回内部用于同步的锁。
需要注意的是,访问包装后的ctypes对象会比直接访问原来的纯 ctypes 对象慢得多。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值