Linux 上 flock
和 Windows 上 FileStream.Lock
行为不一致
Linux 的
flock是 advisory(顾问锁),依赖所有参与者主动检查;Windows 的
FileStream.Lock默认是 mandatory(强制锁),但仅对同一句柄有效,跨进程时实际也退化为 advisory。两者底层语义接近,但 API 设计和错误表现差异大——比如 Linux 下重复
flock不报错,Windows 下对已锁定文件再次
Lock会直接抛
IOException。
实操建议:
别混用flock(通过 P/Invoke)和
FileStream.Lock:它们锁的不是同一个内核对象,完全不互斥 统一用
FileStream+
FileShare.None+ 异常捕获,这是跨平台最稳的 advisory 锁模拟方式 Linux 下若用
flock,必须确保所有进程都调用
fcntl(F_SETLK),且不能依赖
close()自动释放(因为 .NET 的
FileStream可能延迟 dispose)
FileStream
构造时 FileShare
参数决定锁粒度
很多人以为加了
Lock()就锁住了,其实真正起作用的是打开文件时的
FileShare。Windows 和 Linux 上,
new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None)才能阻止其他进程以读/写方式打开该文件;而
FileShare.Read或
FileShare.Write会让锁形同虚设。
实操建议:
锁文件必须用FileShare.None,哪怕只是想防止写入——因为 Linux 的
flock和 Windows 的句柄锁都基于“打开权限”协商 不要在
using块里只做
Lock()而不保持流打开:一旦
FileStream被 dispose,锁立即释放 若需长时间持有锁但又不想阻塞 I/O,可用
FileStream打开后立刻
Lock(0, long.MaxValue),然后把流存在静态字典里(注意线程安全和泄漏)
跨平台锁失败时的典型错误信息和应对
Linux 下常见
IOException: The process cannot access the file because it is being used by another process(这是 .NET 在 Linux 上对
EBUSY的翻译,实际和 Windows 错误码一样);Windows 下还可能遇到
Access to the path is denied(权限不足或防病毒软件拦截)。
实操建议:
捕获IOException,检查
ex.HResult:Windows 上
-2147024864(0x80070020)表示被占用,Linux 上
HResult通常为 0,得靠
ex.Message.Contains("used by another process")
避免轮询重试时用固定 sleep:改用指数退避(如 10ms → 30ms → 100ms),否则在容器或高并发场景下容易打满 CPU
测试时务必在 WSL、Docker(alpine/debian)、Windows Server 上分别验证——尤其是 Docker 默认以 root 运行,而生产环境常以非 root 用户跑,权限差异会导致锁行为突变
为什么不用 MemoryMappedFile
或临时文件做协调
MemoryMappedFile在 Linux 上需要
/dev/shm支持,且跨进程命名规则与 Windows 不兼容;临时文件(如
file.lock)看似简单,但存在竞态:两个进程同时检查文件不存在 → 同时创建 → 都认为自己拿到锁。
实操建议:
真要临时文件方案,必须用原子操作:File.Create("file.lock", 0, FileOptions.DeleteOnClose | FileOptions.Asynchronous),并捕获 IOException判断是否被抢占(Linux/macOS 下成功则拿到锁,Windows 下需额外判断
Win32Exception.NativeErrorCode == 5) 如果业务允许,优先用外部协调服务(Redis 的
SET key val NX PX 30000)代替文件锁——文件锁本质是妥协方案,只适合无外部依赖的嵌入式或单机工具 所有锁逻辑必须带超时:无论是
FileStream的打开超时,还是业务层的租约续期,否则一个崩溃进程可能永久霸占锁
真正麻烦的不是怎么加锁,而是怎么让所有调用方都遵守同一套打开+检查+释放流程。漏掉任意一环,顾问锁就变成幻觉。
