9人参与 • 2026-01-31 • Python
“刚写入的配置文件,读取时居然一半是空的?”—— 这是 linux 开发中高频踩坑的场景。根源在于文件操作并非原子行为,当写进程正在执行磁盘 i/o 时,读进程恰好介入,就会捕获到文件的中间状态。
举个直观例子:写进程要写入 “hello world”,实际会拆分为多次磁盘写入操作。若读进程在 “hello” 已写入但 “ world” 未完成时触发读取,就会得到残缺的 “hello”。这种因执行顺序不确定导致的问题,在操作系统中被称为竞态条件(race condition) 。
更危险的是,多进程同时写入时,不仅会出现读取残缺,还可能导致数据覆盖甚至文件损坏 —— 就像多人同时在同一页纸上写字,最终内容必然混乱不堪。
解决并发读写问题的核心是同步机制,即保证读写操作不会 “撞车”。以下三种方案覆盖了不同场景需求,按推荐优先级排序。
这是最稳妥的 “笨办法”,核心逻辑是 “先改副本,再换原件”,完美规避直接操作原文件的风险。
工作流程
data.txt.tmp),避免直接修改data.txt;os.replace()(linux 下底层调用rename系统调用)将临时文件重命名为原文件。为什么安全?
rename操作在 ext4、xfs 等主流文件系统中是原子的 —— 要么完全替换,要么完全不替换,不存在中间状态;当文件过大(复制临时文件成本高)或需频繁更新时,文件锁是更优选择。它通过 “锁定 - 操作 - 释放” 的流程,控制对文件的并发访问。
常见锁类型对比
| 锁类型 | 特点 | 适用场景 |
|---|---|---|
| 共享锁(读锁) | 多进程可同时持有,阻止写操作 | 多进程并发读取 📖 |
| 排他锁(写锁) | 仅单进程持有,阻止所有读写操作 | 单进程写入或更新 ✍️ |
python 实现推荐
原生fcntl模块仅支持 linux,跨平台场景建议用filelock库:
threading.lock的线程级限制);若文件存储的是用户信息、配置项等结构化数据,直接用数据库(如 sqlite、redis)是 “偷懒却高效” 的选择。数据库内置了完善的:
这种方式将并发控制交给专业工具,大幅减少业务代码复杂度。
结合tempfile模块实现临时文件自动管理,避免手动清理残留文件:
import os
import tempfile
import time
def safe_write(filepath, content):
"""安全写入文件:临时文件+原子替换 🛡️"""
# 创建自动清理的临时文件(suffix指定后缀,dir确保同文件系统)
with tempfile.namedtemporaryfile(
suffix=".tmp",
dir=os.path.dirname(filepath),
delete=false, # 手动控制删除时机 ⏳
mode="w",
encoding="utf-8"
) as tmp_f:
# 模拟耗时写入过程
for char in content:
tmp_f.write(char)
tmp_f.flush() # 强制刷入磁盘 💾
print(f"[写进程] 已写入: {tmp_f.tell()}/{len(content)}", end="\r")
time.sleep(0.1)
try:
# 确保数据完全落盘(避免内存缓存导致的替换异常)
with open(tmp_f.name, "r") as f:
os.fsync(f.fileno())
# 原子替换原文件 🔄
os.replace(tmp_f.name, filepath)
print(f"\n[写进程] 替换完成,{filepath} 已更新 ✅")
except exception as e:
# 异常时清理临时文件 🧹
os.unlink(tmp_f.name)
raise runtimeerror(f"写入失败: {e} ❌")
# 测试:同时运行读写进程
if __name__ == "__main__":
target = "data.txt"
# 初始化原文件
if not os.path.exists(target):
with open(target, "w") as f:
f.write("初始内容")
print(f"[写进程] 已创建初始文件 {target} 📄")
# 模拟写入
safe_write(target, "这是完整的新内容,不会被读残缺!")
搭配读者进程测试效果:
import time
def continuous_read(filepath):
"""持续读取文件,观察内容变化 📖"""
print("=== 读进程启动 ===")
try:
while true:
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
print(f"[读进程] 内容: {repr(content)}")
time.sleep(0.5) # 间隔0.5秒读取一次 ⏱️
except keyboardinterrupt:
print("\n读进程退出 🚪")
if __name__ == "__main__":
continuous_read("data.txt")
运行效果:读进程始终显示 “初始内容” 或完整新内容,绝不会出现残缺片段。
安装依赖:pip install filelock
from filelock import filelock
import time
def write_with_lock(filepath, content):
"""加排他锁写入文件 🔐"""
# 锁文件与目标文件同目录,便于管理
lock_path = f"{filepath}.lock"
with filelock(lock_path, timeout=5): # 超时5秒避免死锁 ⏱️
print("[写进程] 获得锁,开始写入 ✍️")
with open(filepath, "w", encoding="utf-8") as f:
for char in content:
f.write(char)
f.flush() # 强制刷盘 💾
time.sleep(0.1)
print("[写进程] 写入完成,释放锁 🗝️")
def read_with_lock(filepath):
"""加共享锁读取文件 📖"""
lock_path = f"{filepath}.lock"
with filelock(lock_path, shared=true, timeout=5): # 共享锁允许多进程读 🤝
print("[读进程] 获得共享锁,开始读取 👀")
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
print(f"[读进程] 读取内容: {repr(content)}")
time.sleep(1) # 模拟读取耗时 ⏳
原子替换的跨设备陷阱 :os.replace()仅在同一文件系统内保证原子性。若临时目录(如/tmp)与目标文件目录在不同磁盘,会抛出atomicmovenotsupportedexception。解决方案:用tempfile的dir参数指定临时文件与目标文件同目录 。
锁文件的残留问题 :手动创建锁文件易因进程崩溃导致残留,推荐用filelock库 —— 它会在进程退出时自动清理锁文件,nfs 文件系统场景也能有效避免.nfs残留文件 。
数据落盘的隐藏坑:即使write()调用成功,数据可能仍在内存缓存中。务必用f.flush()+os.fsync(f.fileno())强制刷入磁盘,避免替换后读取到缓存中的旧数据 。
windows 与 linux 的兼容性:跨平台开发时,fcntl模块不可用,filelock是更好选择;路径分隔符用os.path模块处理,避免硬编码/导致 windows 报错。
| 场景 | 推荐方案 | 优点 |
|---|---|---|
| 配置文件更新、日志轮转 | 临时文件 + 原子替换 | 简单安全,无锁依赖 |
| 大文件频繁更新、多进程读写 | 文件锁(filelock) | 减少复制开销,支持并发读 |
| 结构化数据存储(用户信息等) | 数据库(sqlite/redis) | 自带事务与崩溃恢复 |
文件并发读写的核心不是 “禁止并发”,而是 “有序并发” 。掌握临时文件替换的简单可靠,理解文件锁的灵活控制,善用数据库的专业能力 —— 这三种武器足以应对 99% 的 linux 文件操作场景 。
到此这篇关于从原理到实战详解python中文件并发读写的避坑指南的文章就介绍到这了,更多相关python文件并发读写内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论