朕喜技术(ZhenxiJS)
2026.09.05
1 分钟阅读

Python 多进程并发「读-改-写」共享文件丢失更新 Lost Update —— 用 flock 文件锁修复

日期:2026-09-05 | 技术栈:Python3 / Docker / 多进程 / flock 文件锁 分支:2026-09-05-1600 关键词:多进程并发、读改写竞态、Lost Update、丢失更新、flock、文件锁、共享计数器、原子操作、race condition、Read-Modify-Write、并行批处理

一、问题背景

在并行批处理任务中,多个 worker 进程并发往同一个「进度/结果计数器」文件累加数值,但最终统计总数远小于理论值。

典型场景:8 个 worker 各累加 200 次(理应 +1600),跑完后文件里却只多了几个 —— 绝大多数累加被"吞掉"了。

这不是数值溢出,而是经典的并发 Read-Modify-Write(读-改-写)丢失更新(Lost Update):多个进程几乎同时读到同一个旧值,各自 +1 后再写回,后写的把先写的覆盖了。

二、复现步骤

# 1. 拉取分支并进入场景目录
git fetch origin && git checkout 2026-09-05-1600
cd scenario/2026-09-05-1600

# 2. 启动 Docker 环境
docker compose up -d --build

# 3. 进容器复现
docker compose exec app bash
bash /workspace/reproduce.sh

复现输出:

>>> 期望最终值(应无丢失更新): 1600
>>> 实际最终值:               3
>>> 发生了【丢失更新】!实际值比期望少了 1597

连续多次运行,实际值在 1~12 之间波动,稳定复现。

Bug 代码(worker_buggy.py 核心逻辑)

for _ in range(args.iter):
    v = read_counter(COUNTER)      # 读当前值
    write_counter(COUNTER, v + 1)  # 写回 v+1

无任何锁 / 原子性保证,高并发下必然互相覆盖。

三、根本原因

并发 Read-Modify-Write(读-改-写)竞态条件 导致丢失更新。

时刻进程 A进程 Bcounter.txt
t0读到 v=55
t1读到 v=55
t2写回 66
t3写回 66

A、B 各自 +1,但文件只从 5 变成 6,丢失了一次累加。这就是 check-then-act(检查再更新)不是原子操作的本质问题。

四、解决办法

核心思路:让整套读-改-写成为一个临界区,而不是只锁某一步。

修复代码(worker_fixed.py,使用 flock 排它锁)

import fcntl

def locked_increment(path):
    with open(path, "r+") as f:
        fcntl.flock(f, fcntl.LOCK_EX)   # 加排它锁,临界区开始
        try:
            v = int(f.read().strip() or "0")
            f.seek(0)
            f.write(str(v + 1))
            f.truncate()
        finally:
            fcntl.flock(f, fcntl.LOCK_UN)  # 释放锁

其他可选方案

方案适用场景说明
flock 排它锁多进程同机共享文件推荐,最直接可靠
O_APPEND 原子追加逐条追加记录不适合"累加再回写"
数据库原子命令分布式/多主机redis INCR / SQL 原子 UPDATE
fcntl 记录锁跨进程锁定用于有锁文件的进程间协调

五、验证

修复后运行验证脚本:

bash /workspace/verify.sh

输出:

期望=1600 实际=1600
PASS ✓ (无丢失更新)

连续 5 次验证全部通过,修复方案稳定可靠。

六、经验与避坑

  1. 锁必须覆盖完整读-改-写:只锁「读」或只锁「写」都不行,必须把 读→计算→写回 作为一个整体包在锁内。
  2. Python 文件模式要注意:flock 需要 "r+" 模式(可读可写)。用 "w" 模式会截断文件。
  3. 写完后记得 truncate():新值比旧值短时(如 999→100),不 truncate 会残留旧尾字符。
  4. 竞态 Bug 测试要反复跑:并发竞态结果不稳定(每次丢多丢少不同),验证时建议多次运行确认。
  5. flock 跨进程有效、跨主机无效:flock 是基于文件的,只在同一文件系统上的进程间有效。NFS/分布式场景需用分布式锁(Redis/etcd/ZooKeeper)。
  6. 批量脚本如果频繁做这种操作,建议换数据库:用 redis INCR 或 SQL 原子操作,天然避免此类问题。
  7. Shell 脚本常见坑:v=$(cat x); echo $((v+1)) > x 就是典型的非原子读-改-写,多进程并行时必踩 Lost Update。

版权声明

本文为博主原创文章,遵循 CC BY-NC-SA 4.0 许可协议。 转载请注明出处:http://localhost/blogs/2026-09-05-1600-multiprocess-lost-update-flock

分享这篇文章