Python SharedMemoryDict多进程共享PIL对象二次访问报错求解
问题说明
- 场景:部署Flask服务时使用Pillow加载图片到内存,需求是每次收到请求创建子进程时,可复用内存中已加载的PIL对象,初始选型为shared-memory-dict库实现共享内存存储
- 现象:先启动父进程写入PIL对象到共享内存,第一次启动子进程可正常读取对象,第二次启动子进程时抛出
KeyError: 'source_img',表现为子进程退出后共享内存中存储的条目被清除
复现代码
父进程代码
import time from shared_memory_dict import SharedMemoryDict from PIL import Image if __name__ == '__main__': print("Starting PARENT") source_img = Image.open('d:/input_image.png') size = 1024 * 1024 * 16 smd = SharedMemoryDict(name='shared_mem', size=size) smd['source_img'] = source_img print("PARENT started!") source_img.show() time.sleep(99999)
子进程代码
from shared_memory_dict import SharedMemoryDict if __name__ == '__main__': print("Starting CHILD") size = 1024 * 1024 * 16 smd = SharedMemoryDict(name='shared_mem', size=size) existing_smd = SharedMemoryDict(name='shared_mem', size=size) source_img = existing_smd['source_img'] source_img.show()
报错信息
Starting CHILD Traceback (most recent call last): File "child_dict.py", line 23, in <module> source_img = existing_smd['source_img'] File "/data/anaconda3/envs/STR_3_8/lib/python3.8/site-packages/shared_memory_dict/dict.py", line 92, in __getitem__ return self._read_memory()[key] KeyError: 'source_img'
问题根因
- 共享内存被误回收:shared-memory-dict默认在实例被垃圾回收、进程退出时自动调用
unlink()标记删除关联的共享内存段。子进程代码中没有声明自身非共享内存的所有者,还重复创建了两个连接同一内存段的实例,子进程退出时会直接把父进程创建的共享内存段回收,第二次启动子进程自然读不到原有数据。 - 存储方式不符合需求:shared-memory-dict基于pickle序列化存储对象,直接存入PIL Image对象时,会把图片所有像素数据序列化后拷贝到共享内存,子进程读取时再反序列化拷贝出一份新的PIL对象,本质没有实现内存复用,还额外增加了序列化开销,大图片场景下性能极差。
解决方案
根据部署场景选择对应方案即可:
方案1:修正shared-memory-dict配置(仅适合小对象共享,不推荐存大尺寸PIL图)
如果只是要修复现有代码的报错,只需要修改子进程的实例初始化逻辑,关闭子进程的自动清理权限:
from shared_memory_dict import SharedMemoryDict if __name__ == '__main__': print("Starting CHILD") size = 1024 * 1024 * 16 # 子进程连接共享内存时必须设置unlink_on_close=False,声明自己不是内存所有者,退出时不删除共享段 smd = SharedMemoryDict(name='shared_mem', size=size, unlink_on_close=False) source_img = smd['source_img'] source_img.show()
注意:父进程要保持SharedMemoryDict实例常驻不被GC,仅在服务退出时再手动调用smd.shm.unlink()清理共享内存。
方案2:利用Linux Fork写时复制机制(Flask部署场景最优,零额外开销)
这是Python多进程服务复用只读大对象的通用标准方案,完全不需要引入第三方共享内存库:
- 如果用Gunicorn部署Flask,开启
--preload参数,在全局作用域提前加载PIL图片到变量,Gunicorn会在加载完全局代码后再fork出所有worker进程,Linux的写时复制(COW)机制会保证所有worker进程共享同一块物理内存存储的图片数据,只要代码不修改该图片对象,就不会触发内存拷贝,内存占用和访问性能都是最优的。 - 如果是请求到达时动态创建子进程,使用
fork模式创建进程(Windows不支持fork模式),在创建子进程前就把PIL图片加载为父进程的全局变量,fork出的子进程可直接访问该全局对象,自动实现内存复用。
示例代码:
import multiprocessing as mp from PIL import Image # 父进程提前加载图片到全局 source_img = Image.open('input_image.png') def request_worker(): # 子进程直接访问全局的source_img,只读场景下无内存拷贝 source_img.show() if __name__ == '__main__': # 强制用fork模式创建进程 ctx = mp.get_context('fork') # 第一次请求创建子进程 p1 = ctx.Process(target=request_worker) p1.start() p1.join() # 第二次请求创建子进程,依然可以正常访问复用内存的图片 p2 = ctx.Process(target=request_worker) p2.start() p2.join()
方案3:跨独立进程共享图片零拷贝(适合非fork场景、Windows部署场景)
如果需要在完全独立启动的进程间共享图片,不要直接序列化存储PIL对象,直接共享原始像素内存:
- 父进程加载图片后,提取图片的
mode、size参数,以及原始像素字节流 - 用
multiprocessing.SharedMemory申请对应大小的共享内存块,把像素字节写入共享内存,把图片元数据存在可跨进程访问的存储中 - 子进程连接同一共享内存块,直接基于共享内存的内存视图构造PIL对象,避免像素数据拷贝
内容的提问来源于stack exchange,提问作者Yevgeni Burshtein
相关产品推荐
相关产品推荐

