You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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'
问题根因
  1. 共享内存被误回收:shared-memory-dict默认在实例被垃圾回收、进程退出时自动调用unlink()标记删除关联的共享内存段。子进程代码中没有声明自身非共享内存的所有者,还重复创建了两个连接同一内存段的实例,子进程退出时会直接把父进程创建的共享内存段回收,第二次启动子进程自然读不到原有数据。
  2. 存储方式不符合需求: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()

方案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对象,直接共享原始像素内存:

  1. 父进程加载图片后,提取图片的mode、size参数,以及原始像素字节流
  2. 用multiprocessing.SharedMemory申请对应大小的共享内存块,把像素字节写入共享内存,把图片元数据存在可跨进程访问的存储中
  3. 子进程连接同一共享内存块,直接基于共享内存的内存视图构造PIL对象,避免像素数据拷贝

内容的提问来源于stack exchange,提问作者Yevgeni Burshtein

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 20:01:02