Python中如何安全利用文件系统作共享内存,让消费脚本感知写入完成?
我有一个Python脚本(渲染服务器),负责接收生成图像及关联数据的请求;另有一个独立的Python应用(消费者),需要使用这些生成的数据。消费者不知道新数据何时可用,理想情况下无需知晓渲染服务器的存在,只需知道数据会按约定方式提供。
我原本的方案是:约定一个双方都能访问的outputs目录,渲染服务器在该目录下创建带时间戳的子目录,并将多个文件写入其中。
渲染服务器核心代码:
os.makedirs('outputs/' + timestamped_subdir) # 向该目录写入多个文件
消费者核心代码:
dirs = set() while True: new_dirs = set(glob('outputs/*')).difference(dirs) if not len(new_dirs): continue # 处理最新子目录中的内容
但问题是,消费者会在渲染服务器完成所有文件写入前就检测到子目录,进而访问时触发FileNotFoundError。我尝试过先在临时目录写入文件,再复制到outputs目录:
os.makedirs('temp') # 原代码笔误os.makedisr已修正 # 向临时目录写入文件 shutil.copytree('temp', 'outputs/' + timestamped_subdir)
但消费者还是会在文件复制完成前感知到目标子目录的存在,同样报错。后来我换成shutil.move替代shutil.copytree,似乎解决了问题,但不清楚底层机制,不确定是否可靠。想知道实现这个需求的正确方式是什么?
几种可靠的实现方式
1. 利用文件系统的原子移动操作(推荐)
你换成shutil.move的思路是完全正确的,核心原因是同一文件系统内的目录/文件移动操作是原子性的:
- 当你在和
outputs同分区的临时目录写完所有文件后,执行shutil.move时,文件系统只会修改目录的元数据,不会实际移动文件内容,整个操作瞬间完成。 - 对消费者来说,
outputs目录下的子目录要么完全不存在,要么已经是包含所有文件的完整状态,不会出现“目录存在但文件不全”的中间态,从根本上避免了FileNotFoundError。
注意:如果临时目录和outputs目录不在同一个文件系统(比如不同磁盘分区),shutil.move会退化为复制+删除,此时就不是原子操作了。所以要确保临时目录和目标目录在同一分区。
修正后的渲染服务器代码:
# 在outputs目录下创建临时子目录,确保和目标目录同分区 temp_dir = f'outputs/_temp_{timestamped_subdir}' os.makedirs(temp_dir) # 向临时目录写入所有文件 # 原子移动到目标位置 shutil.move(temp_dir, f'outputs/{timestamped_subdir}')
2. 写入完成后创建标记文件
如果因为某些原因无法使用原子移动,可以在渲染服务器写完所有文件后,在目标目录下创建一个特殊的标记文件(比如_COMPLETED_),消费者只处理包含该标记文件的子目录:
渲染服务器补充代码:
os.makedirs(f'outputs/{timestamped_subdir}') # 写入所有文件 # 创建完成标记文件 open(f'outputs/{timestamped_subdir}/_COMPLETED_', 'w').close()
消费者修改代码:
dirs = set() while True: all_dirs = set(glob('outputs/*')) new_dirs = all_dirs.difference(dirs) dirs = all_dirs for dir_path in new_dirs: # 检查是否存在完成标记 if os.path.exists(os.path.join(dir_path, '_COMPLETED_')): # 处理该目录内容
3. 使用文件锁机制
可以借助fcntl(Linux/macOS)或msvcrt(Windows)的文件锁,让渲染服务器在写入文件时加锁,消费者尝试获取锁,只有成功获取后才处理文件。不过这种方式需要消费者和服务器配合处理锁逻辑,复杂度稍高。
4. 基于消息队列的通知机制
如果系统允许引入额外组件,可以用消息队列(比如Redis的Pub/Sub、RabbitMQ),渲染服务器完成文件写入后,向队列发送一条消息,消费者监听队列,收到消息后再去读取文件。这种方式能避免消费者轮询,效率更高,但需要额外的服务依赖。
内容的提问来源于stack exchange,提问作者Alexander Soare

