【Python】多进程-进程间共享对象

5,134 阅读3分钟

「这是我参与2022首次更文挑战的第12天,活动详情查看:2022首次更文挑战」。

《【Python】多进程的同步》 中,讲解了使用互斥锁和信号量来进行进程之间的同步,下面我们来解决在多进程状态下,出现找不到变量的问题。

消失的全局变量

《【Python】线程间同步的实现与代码》中,演示了使用多线程对全局变量进行加法操作的代码,现在我们修改一下代码,使用多进程来实现这一操作:

import multiprocessing

def add(lock):
    """ num 数值每次加 1,执行 100000 次"""
    global num
    for _ in range(100000):
        if lock.acquire():
            num += 1
            lock.release()

if __name__ == "__main__":
    num = 0
    lock = multiprocessing.Lock()  # 创建一个锁

    p1 = multiprocessing.Process(target= add, args=(lock, ))
    p2 = multiprocessing.Process(target= add, args=(lock, ))

    # 开始进程
    p1.start()
    p2.start()

    # 等待进程结束
    p1.join()
    p2.join()

    print(num)

运行代码,我们会发现,程序报错了:NameError: name 'num' is not defined

为什么会出现这种情况呢?回忆一下操作系统里的内容:

线程是操作系统资源分配的基本单位

也就是说, num 这个变量只存在与主线程中,而新创建的 p1p2 进程不能直接访问主进程的资源,所以找不到 num 这个变量。

进程之间共享对象 - 共享内存

multiprocessing 中提供了 ValueArray,可以将数据存储在共享内存映射中,供全部进程访问。下面,我们将 num 这个变量的值保存在共享内存映射中:

import multiprocessing

def add(n, lock):
    """ num 数值每次加 1,执行 100000 次"""
    for _ in range(100000):
        if lock.acquire():
            n.value += 1
            lock.release()
            
if __name__ == "__main__":
    num = multiprocessing.Value('i', 0)
    lock = multiprocessing.Lock()  # 创建一个锁

    p1 = multiprocessing.Process(target= add, args=(num, lock, ))
    p2 = multiprocessing.Process(target= add, args=(num, lock, ))

    # 开始进程
    p1.start()
    p2.start()

    # 等待进程结束
    p1.join()
    p2.join()

    print(num.value)    #200000

multiprocessing.Value(typecode_or_type,  *args, lock=True) 返回一个从共享内存上创建的 ctypes 对象。可以通过 value 属性访问这个对象本身。

  • typecode_or_type :返回的对象类型,如 i(int),f(float),d(doble)
  • lock 
    • 默认值为 True, 此时会新建一个递归锁用于同步对于此值的访问操作。
    • 如果是 Lock 或者 RLock 对象,那么这个传入的锁将会用于同步对这个值的访问操作
    • 如果是 False , 那么对这个对象的访问将没有锁保护,也就是说这个变量不是进程安全的

multiprocessing.Array(typecode_or_type, size_or_initializer,  * , *lock=True*)Value() 相似,只不过 Value() 的第二个参数会被当作初始值,而 Array() 的第二个参数,如果是一个整数,那就会当做数组的长度,并且整个数组的内存会初始化为0。否则,会被当成一个序列用于初始化数组中的每一个元素。

由于 Value() 默认会新建一个递归锁用于同步对于此值的访问操作,所以,我们可以将 add 函数简化为:

def add(n):
    """ num 数值每次加 1,执行 1000 次"""
    for _ in range(1000):
        with n.get_lock():
            n.value += 1

进程之间共享对象 - 服务进程

除了共享内存的方式,另一种进程间共享对象的方式是使用服务进程:由 Manager() 返回的管理器对象控制一个服务进程,该进程保存Python对象并允许其他进程使用代理操作它们。

Manager() 返回的管理器支持的类型有: list、 dict、 Namespace 、 Lock、 RLock、 Semaphore、 BoundedSemaphore、 Condition 、 Event、 Barrier、 Queue、 Value 和 Array

下面,我们使用 Manager() 来实现加一的操作:

import multiprocessing

def add(n, lock):
    """ num 数值每次加 1,执行 1000 次"""
    for _ in range(1000):
        if lock.acquire():
            n.value += 1
            lock.release()

if __name__ == "__main__":

    lock = multiprocessing.Lock()  # 创建一个锁

    with multiprocessing.Manager() as manager:
        num = manager.Value('i', 0)

        p1 = multiprocessing.Process(target= add2, args=(num, lock))
        p2 = multiprocessing.Process(target= add2, args=(num, lock))

        # 开始进程
        p1.start()
        p2.start()

        # 等待进程结束
        p1.join()
        p2.join()

        print(num.value)   # 2000