多线程写同一个文件时如何使用 flock 实现文件锁

多线程/进程直接写同一文件会因写操作非原子性导致错乱、覆盖、截断;flock是内核级建议性锁,需用文件描述符全程包裹写入动作并保持打开,避免子进程释放锁失效。

为什么直接多线程写同一个文件会出问题

多个线程(或进程)同时 fwriteecho >> 同一个文件,不加协调时会出现内容错乱、覆盖、截断——因为写操作不是原子的:open → lseek → write → close 中间可能被抢占。Linux 的 flock 是内核级建议性锁,它不阻止你打开文件,但能让你在关键区前「协商等待」。

flock 在 shell 脚本里怎么用才安全

必须用 flock 包裹整个写入动作,且锁文件描述符要保持打开状态直到写完。常见错误是把 flock 当成命令前缀单独调用,比如 flock /path/file -c 'echo data >> /path/file' —— 这会 fork 子进程,锁在子进程退出后立即释放,根本没保护写入。

  • 正确做法:用文件描述符 + -w 设置超时,避免死等:
    exec 200>/path/lockfile
    flock -w 5 200 || { echo "lock timeout"; exit 1; }
    echo "data" >> /path/target.txt
    flock -u 200  # 显式解锁(可选,fd 关闭时自动释放)
  • 锁文件不必和目标文件是同一个,但得是同一文件系

    统上的真实文件(不能是符号链接或 NFS 挂载点,NFS 下 flock 可能失效)
  • 如果脚本异常退出,fd 200 会自动关闭,锁随之释放,这点比 fcntl 更省心

在 C/Python 里调用 flock 的关键差异

系统调用 flock() 和 shell 的 flock 命令行为一致,但语言绑定常有坑:

  • C 中必须对已打开的 int fd 调用 flock(fd, LOCK_EX),不能对路径名调用;且 fork() 后子进程会继承锁,但 exec() 后锁保留(这点和 shell 不同)
  • Python 的 fcntl.flock() 实际调用的是 fcntl(F_SETLK),不是 flock() 系统调用 —— 它在 NFS 上更可靠,但锁类型(LOCK_EX/LOCK_SH)语义相同;而 os.system("flock ...") 是启动新进程,锁生命周期不跨调用,基本没用
  • 所有语言中,锁只对「通过同一 inode 打开的文件描述符」有效;重命名文件不影响锁,但 mv /old /new && echo > /old 这种重建操作会丢锁

什么情况下 flock 会失效或不适用

flock 是建议性锁,完全依赖协作。只要有一个写入方不用它,锁就形同虚设。更隐蔽的问题是:

  • 容器环境里,若多个容器挂载了同一宿主机路径,但未共享文件系统锁(如 overlayfs),flock 在容器间无效
  • Go 默认用 syscall.Flock,但某些构建 tag(如 netgo)下可能绕过系统调用,导致锁失效
  • 日志轮转工具(如 logrotate)执行 mv + touch 时,新文件无锁,后续写入直接冲进去——必须让轮转也参与 flock 协商,或改用 copytruncate 模式

真正需要强一致性时,flock 只是第一道防线;高并发场景建议改用消息队列或单写入进程转发,文件锁只适合低频、短临界区的协作。