Python多进程编程中共享内存与进程同步初探

261 阅读6分钟

摘要

在Python多进程编程中,高效且安全地共享数据是一个常见需求。本文将通过一个完整的示例,初步探讨如何使用multiprocessing.shared_memory模块实现进程间数据的共享,并通过同步原语 确保数据一致性和资源安全释放。

共享内存基础:为什么需要它?

在多进程环境中,每个进程都有自己独立的内存空间。传统的变量和数据结构无法再进程间共享,这就导致了进程间通信(IPC)的需求。虽然Python提供了诸如队列(Queue)、管道(Pipe)等IPC机制,但对于大数据量的共享,这些方法的性能可能不够理想。

共享内存(Shared Memory)是一种高效的IPC方式,它允许不同进程访问同一块物理内存区域。通过共享内存,过个进程可以直接读写同一份数据,避免了数据复制的开销,显著提高了数据传输的效率。

在Python中,我们可以使用multiprocessing.shared_memory模块来实现共享内存功能。下面是一个完整示例

import numpy as np
from multiprocessing import Process, Manager
from multiprocessing.shared_memory import SharedMemory
import time
import random

def create_shared_memory(size, dtype=np.int32, name=None):
    """创建共享内存并返回共享内存对象和NumPy数组视图"""
    # 创建一个本地NumPy数组作为模板
    arr = np.zeros(size, dtype=dtype)
    # 创建共享内存块,指定名称或自动生成
    shm = SharedMemory(create=True, size=arr.nbytes, name=name)
    # 在共享内存上创建NumPy数组视图
    shared_arr = np.ndarray(arr.shape, dtype=arr.dtype, buffer=shm.buf)
    # 将本地数组的值复制到共享内存
    np.copyto(shared_arr, arr)
    return shm, shared_arr

def writer_process(shm_name, shape, dtype, barrier, counter):
    """写入进程:向共享内存写入随机数据"""
    try:
        # 连接到已存在的共享内存
        shm = SharedMemory(name=shm_name)
        # 创建共享内存上的NumPy数组视图
        shared_arr = np.ndarray(shape, dtype=dtype, buffer=shm.buf)
        
        # 等待所有进程准备好
        barrier.wait()
        print("写入进程: 开始工作")
        
        for i in range(5):
            # 模拟写入操作(写入随机整数)
            value = random.randint(100, 999)
            shared_arr[0] = value
            print(f"写入进程: 在位置0写入值 {value}")
            
            # 增加计数器,表示完成一次写入
            counter.value += 1
            
            # 随机等待一段时间,模拟处理延迟
            time.sleep(10)
        
        # 关闭共享内存连接
        shm.close()
        print("写入进程: 已完成")
    except Exception as e:
        print(f"写入进程错误: {e}")

def reader_process(shm_name, shape, dtype, barrier, counter, exit_event):
    """读取进程:从共享内存读取数据并验证"""
    try:
        # 连接到已存在的共享内存
        shm = SharedMemory(name=shm_name)
        # 创建共享内存上的NumPy数组视图
        shared_arr = np.ndarray(shape, dtype=dtype, buffer=shm.buf)
        
        # 等待所有进程准备好
        barrier.wait()
        print("读取进程: 开始工作")
        
        last_value = None
        read_count = 0
        
        while not exit_event.is_set():
            # 检查计数器,只有当有新写入时才读取
            current_count = counter.value
            if current_count > read_count:
                current_value = shared_arr[0]
                if current_value != last_value:
                    print(f"读取进程: 在位置0读取到新值 {current_value}")
                    last_value = current_value
                read_count = current_count
            
            # 短暂休眠,减少CPU使用率
            time.sleep(0.1)
        
        # 关闭共享内存连接
        shm.close()
        print("读取进程: 已完成")
    except Exception as e:
        print(f"读取进程错误: {e}")

def main():
    try:
        # 创建管理器,用于进程间同步
        with Manager() as manager:
            # 创建屏障,确保所有进程同时开始
            barrier = manager.Barrier(2)  # 2个工作进程 + 1个主进程
            # 创建计数器,记录写入次数
            counter = manager.Value('i', 0)
            # 创建退出事件,通知读取进程何时停止
            exit_event = manager.Event()
            
            # 创建共享内存(1个整数大小)
            shm, shared_arr = create_shared_memory(1, np.int32, "my_shared_memory")
            print(f"主进程: 共享内存已创建,名称为 '{shm.name}'")
            
            # 创建并启动写入进程
            writer = Process(target=writer_process, 
                            args=(shm.name, shared_arr.shape, shared_arr.dtype, barrier, counter))
            writer.start()
            
            # 创建并启动读取进程
            reader = Process(target=reader_process, 
                            args=(shm.name, shared_arr.shape, shared_arr.dtype, barrier, counter, exit_event))
            reader.start()
            
            # 等待写入进程完成
            writer.join()
            
            # 通知读取进程可以退出
            exit_event.set()
            # 等待读取进程完成
            reader.join()
            
            # 关闭并释放共享内存
            shm.close()
            shm.unlink()
            print("主进程: 共享内存已释放")
            
    except KeyboardInterrupt:
        print("\n程序被用户中断")
    except Exception as e:
        print(f"主进程错误: {e}")

if __name__ == "__main__":
    main()

代码解析

这个示例实现了一个简单的生产者-消费者模型,执行输出是这样的 image.png

代码里有几块说明

  • 写入进程(生产者):向共享内存中写入随机数
  • 读取进程(消费者):从共享内存中读取数据,并在检测到新的值时打印
  • 主进程:负责创建共享内存、启动子进程、并协调他们的执行顺序。



同步原语详解

1、Barrier(屏障)

Barrier用于让多个进程在某个执行点上互相等待,直到所有进程都达到该点才继续执行,在示例中

barrier = manager.Barrier(2)  # 需要2个进程到达屏障点

每个进程通过调用barrier.wait()进入等待状态,当所有2个进程都调用了wait()后,它们会同时被释放,继续执行后续代码。这确保了所有进程在开始读写共享内存前都已就绪

2、Event(事件)

Event是一个简单的标志位,用于进程间通信。在示例中,我们使用exit_event通知读取进程何时停止工作。

exit_event = manager.Event()  # 初始状态为False

# 读取进程中
while not exit_event.is_set():  # 持续读取直到事件被设置
    # 读取共享内存数据

# 主进程中
exit_event.set()  # 设置事件标志为True,通知读取进程退出

这种机制允许主进程在适当时机优雅地终止读取进程,避免强制终止可能导致的资源泄露。

3、join()方法

join()方法用于阻塞当前进程,直到被调用的子进程执行完毕。在示例中,我们按照顺序join()

writer.join()     # 等待写入进程完成
exit_event.set()  # 写入完成后,通知读取进程退出
reader.join()     # 等待读取进程完成

这个顺序非常重要,原因如下

  • 先等待写入进程,确保所有数据都已写入共享内存,保证数据完整性
  • 再通知读取进程退出,确保读取进程有机会处理所有数据
  • 最后等待读取进程,确保读取进程完成资源清理工作,避免资源泄露

4、Manager的功能

Manager是一个高级的进程间通信工具,它通过启动一个独立的管理进程,来协调多个工作进程之间的共享状态。主要功能包括。

1、创建共享数据结构

  • list:共享列表
  • dict: 共享字典
  • Namespace: 共享命名空间对象

2、提供同步元语

  • Lock: 互斥锁
  • RLock:可重入锁
  • Semaphore: 信号量
  • BoundSemaphore: 有界信号量
  • condition: 条件变量
  • Event: 时间
  • Barrier: 屏障

3、数值和字符串共享

  • value:共享单个值
  • Array: 共享数组