发布于2026-07-02 阅读(0)
扫一扫,手机访问
文件锁这事儿,看起来简单,但真要写对代码,坑比想象中多。先抛一个核心判断:别指望靠os.open(..., os.O_EXCL)来搞定跨进程文件锁。
为什么呢?os.O_EXCL这玩意儿,只在创建新文件的那一瞬间才有原子性保障。一旦目标文件已经存在——这在日常开发中太常见了,比如日志文件、状态记录文件——它直接就给你甩一个 OSError: [Errno 17] File exists。说白了,它的设计初衷是“防止文件被重复创建”,而不是“给已存在的文件装把锁”。
更致命的是,它根本管不了跨进程。两个Python进程几乎同时去打开同一个文件,os.O_EXCL毫无约束力,尤其在NFS这类共享文件系统上,它基本等于形同虚设。所以,真正靠谱的方案,还得落到内核级别的文件锁机制上。Linux/macOS 上有 flock,Windows 上有 LockFileEx,而 fcntl.flock 和 portalocker 就是封装了这些底层能力的实用工具。
flock 是POSIX标准下的老兵,Linux和macOS上原生支持,稳定且轻量。但如果你的代码需要跑在Windows上,那就尴尬了——它不支持。portalocker 则是一个聪明的跨平台封装,底层在Linux/macOS调用 flock,在Windows调用 msvcrt.locking 或 WinAPI 的 LockFileEx,自动帮你处理兼容性差异。
一个很实际的选择标准:如果你的代码只跑在纯Linux服务器上,而且不想引入额外依赖,直接用 flock 就够用了。但只要你涉及开发机(macOS)、测试环境(Windows WSL或本地)、或者部署环境不确定,那 portalocker 就能省掉一堆条件判断,避免“本地跑得欢,线上报错懵”的尴尬场面。
另外,有个常见错误现象值得注意:IOError: [Errno 37] No locks a vailable。这通常是因为NFS挂载点禁用了 flock。这种情况下,portalocker 也会跟着失效。解决方案是改用进程间信号量,比如Redis分布式锁,或者在应用层做串行化处理。
使用上的几个要点:
flock(fd, LOCK_EX | LOCK_NB) 可以加非阻塞锁,一旦失败就抛 OSError,适合轮询或超时重试的场景。portalocker.lock(file_obj, portalocker.LOCK_EX | portalocker.LOCK_NB) 行为与 flock 一致,但注意,file_obj 必须是一个已打开的可写文件对象,不能传文件路径字符串。fork 了,记得把旧fd os.close() 掉再重新打开,避免锁的混乱。这是最典型、也最容易翻车的场景:多个进程同时 open(..., 'a') 并 .write(),结果日志内容要么交错在一起,要么部分字节被覆盖。原因在于,'a' 模式本身并不保证原子性——内核只保证单次 write() 系统调用的原子性,但Python的 .write() 可能被拆分成多次系统调用去执行,尤其是在写入大字符串时。
正确的做法,不是“先open再锁”,而是“先以读写模式打开,再加锁,最后写入”。来看一个示例:
import portalocker
def append_log(filepath: str, message: str) -> bool:
try:
with open(filepath, 'r+b') as f: # 必须r+b,a模式无法flock
portalocker.lock(f, portalocker.LOCK_EX | portalocker.LOCK_NB)
f.seek(0, 2) # 移动到文件末尾
f.write((message + '\n').encode('utf-8'))
return True
except (OSError, portalocker.LockException):
return False # 加锁失败,跳过写入或走降级逻辑
这里的 'r+b' 是关键。虽然 flock 对只读打开的文件描述符也有效,但你得能往里面写内容才行。如果文件不存在,open('r+b') 会报 FileNotFoundError,所以需要提前创建文件,比如执行一句 open(filepath, 'a').close() 就搞定了。
很多人误以为 flock 锁会随着 file.close() 自动释放。其实不然。锁是绑定在文件描述符(fd)上的,只要这个fd没有被 os.close(),哪怕Python的file对象已经被垃圾回收或者超出了作用域,锁依然牢牢地攥在手里。更危险的是,fork 之后子进程会复制fd,导致父子进程共持一把锁,很容易引发死锁或者意外的锁释放。
因此,必须显式管理锁的生命周期:
try/finally 结构确保 os.close(fd) 一定会被执行,不要指望垃圾回收器来帮你擦屁股。with open() 语句块里直接调用 flock,因为 __exit__ 只负责关闭file对象,并不会关闭底层的fd(除非你手动调用 os.close(f.fileno()))。flock(fd, LOCK_UN) 可以主动解锁,但一般情况下没必要——只要关闭fd,锁就会自动释放。还有一个容易被忽略的细节:flock 锁的是整个文件,而不是文件中的某一段偏移。如果你需要“只锁定文件中某一块区域”,flock 就无能为力了。这时得用 fcntl.fcntl(fd, F_SETLK, ...) 这种带偏移的记录锁(record locking),但Windows不支持这种方式,portalocker 也没有封装这个功能。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8