Python文件锁不能只靠os.open(..., os.O_EXCL),因为os.O_EXCL仅在创建新文件时原子性生效,对已存在文件会报File exists错误;它不提供跨进程排他控制,尤其在NFS上完全失效;必须依赖flock或portalocker等内核级锁机制。

Python文件锁为什么不能只靠os.open(..., os.O_EXCL)
因为os.O_EXCL只在创建新文件时有效,对已存在文件加锁会直接报错OSError: [Errno 17] File exists;而真实场景中,多数日志或状态文件是预先存在的,需要的是“已有文件的排他写入控制”,不是“仅限首次创建”。
更关键的是:os.O_EXCL不跨进程——它只是open系统调用层面的原子性检查,无法阻止另一个Python进程同时打开同一文件(尤其在NFS等共享文件系统上完全失效)。
所以必须依赖内核级文件锁机制,即flock(Linux/macOS)或LockFileEx(Windows),而fcntl.flock和portalocker正是封装了这些底层能力的可靠选择。
flock vs portalocker:选哪个?看运行环境和需求
flock是POSIX标准接口,Linux/macOS原生支持,但Windows不支持;portalocker是跨平台封装库,底层在Linux/macOS用flock,在Windows用msvcrt.locking或WinAPI LockFileEx,自动处理兼容性。
立即学习“Python免费学习笔记(深入)”;
如果你的代码只跑在Linux服务器上,且不想引入额外依赖,直接用flock更轻量;但只要涉及开发机(macOS)、测试(Windows WSL/本地)、或部署环境不确定,portalocker省去条件判断,避免“本地能跑线上报错”的坑。
常见错误现象:IOError: [Errno 37] No locks available——通常是NFS挂载点禁用了flock,这时portalocker也会失败,需改用进程间信号量(如Redis锁)或应用层串行化。
快速生成专业的 Python 脚本和应用代码。一键创建完整项目结构,支持CLI、API、爬虫、Bot、Django等多种项目类型,包含完整的项目结构、配置文件、依赖管理、测试、README和文档。
-
flock(fd, LOCK_EX | LOCK_NB)加非阻塞锁,失败立即抛OSError,适合轮询或超时重试 -
portalocker.lock(file_obj, portalocker.LOCK_EX | portalocker.LOCK_NB)行为一致,但file_obj必须是已打开的可写文件对象(不能是路径字符串) - 两者都只锁文件描述符,不是文件路径;子进程继承fd时锁也继承,但
fork后需注意是否要os.close()旧fd再重开
用portalocker写一个安全的日志追加函数
这是最典型、最容易出问题的场景:多个进程同时open(..., 'a')并.write(),导致内容交错(比如两行日志挤在同一行,或部分字节被覆盖)。
正确做法不是“先open再锁”,而是“先以读写模式打开,再加锁,最后写”。因为'a'模式本身不保证原子性,内核只保证单次write()系统调用的原子性,但Python的.write()可能分多次系统调用(尤其大字符串)。
import portalocker
<p>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 # 加锁失败,跳过写入或走降级逻辑</p>注意:'r+b'是关键——flock对只读打开的文件描述符也生效,但你得能写进去;如果文件不存在,open('r+b')会报FileNotFoundError,需提前创建(open(filepath, 'a').close()即可)。
fcntl.flock的常见误用和释放时机
很多人以为flock锁会随file.close()自动释放,其实不然:锁绑定在文件描述符(fd)上,只要该fd没被os.close(),即使Python的file对象被del或超出作用域,锁依然持有。更危险的是,fork后子进程会复制fd,父子进程共持一把锁,可能导致死锁或意外解锁。
所以必须显式管理:
- 用
try/finally确保os.close(fd)被执行,不要依赖GC - 避免在
with open()里直接flock,因为__exit__只关file对象,不关fd(除非手动os.close(f.fileno())) -
flock(fd, LOCK_UN)可以主动解锁,但通常没必要——只要关闭fd就自动释放 - 进程崩溃时,内核会自动回收所有fd并释放锁,这点比应用层锁(如Redis)更可靠
真正容易被忽略的是:锁的粒度是整个文件,不是某段偏移。如果你需要“只锁文件某一块”,flock做不到,得用fcntl.fcntl(fd, F_SETLK, ...)带偏移的记录锁(record locking),但Windows不支持,portalocker也不封装这个。

















