如何实现Python中mmap赋值操作的原子性?
关于mmap大区域赋值的原子性问题解答
首先明确你遇到的核心疑问:在一个进程中执行m[:] = b2覆盖整个mmap映射区域时,另一个并行读取的进程是否会读到半更新的内容?结合你的测试代码和Python mmap的实现,结论是这个操作不是原子的,很大概率会读到混合数据。
先拆解你的测试代码场景
你在第一个进程里的核心操作:
import struct import mmap # 生成两个超大整数列表 huge_list1 = [888 for _ in range(100000000)] huge_list2 = [9999 for _ in huge_list1] # 打包成4字节无符号整数的字节串(每个列表对应400MB左右的字节数据) b1 = struct.pack("100000000I", *huge_list1) b2 = struct.pack("100000000I", *huge_list2) # 先写入初始数据到文件 with open('mmp', 'wb') as f: f.write(b1) # 映射文件到内存并覆盖 with open('mmp', 'r+') as f: m = mmap.mmap(f.fileno(), 0) m[:] = b2 # 核心的覆盖操作
另一个进程尝试读取这个mmap文件:
import mmap with open('mmp', 'r') as f: m = mmap.mmap(f.fileno(), 0) # 读取并解析数据
为什么m[:] = b2不是原子操作?
- Python的
mmap.mmap对象切片赋值,底层会调用操作系统的mmap写入接口,但对于这种400MB级别的超大写入,操作系统会自动拆分成多个页级别的写操作(通常内存页大小是4KB)。这些写操作不是一次性完成的,中间可能被调度器打断,其他进程在这个间隙读取,就会拿到部分旧数据(b1)和部分新数据(b2)的混合结果。 - Python官方文档没提及原子性,是因为这属于操作系统层面的行为,Python只是封装了系统的mmap API。无论Linux、Windows还是macOS,都不会保证跨多个内存页的写入操作是原子的——只有单个内存页内的某些特定对齐小数据写操作才可能被保证原子性,但你的场景完全不符合这个条件。
如何实现跨进程的原子读写?
如果需要确保另一个进程要么读到完整的b1,要么读到完整的b2,可以参考这些方案:
- 使用文件锁:在写入进程执行
m[:] = b2前后,用文件锁锁定整个文件。比如在Linux/macOS用fcntl加排他锁,读取进程加共享锁,让读写操作互斥,避免读到半更新数据。 - 双缓冲区模式:准备两个独立的映射区域(比如两个文件或者一个文件的两个分段),写入时先更新其中一个缓冲区,更新完成后用一个原子标记(比如
multiprocessing.Value创建的共享整数)告诉读取进程切换到新的缓冲区。 - 原子替换文件:先把b2写入一个临时文件,然后用
os.replace(原子操作)把临时文件替换成目标文件,再让读取进程重新映射文件。这种方式适合不需要实时映射更新的场景。
内容的提问来源于stack exchange,提问作者Kai
相关产品推荐
相关产品推荐

