前言
Python语言中的文件锁可以使用 fcntl 库,它实际上是对Unix系统上的 fcntl
和 ioctl
函数提供了一个接口。官网描述中是这样形容的:
This module performs file control and I/O control on file descriptors
这个模块提供了针对文件描述符的文件控制和I/O控制。常用的是在多进程/线程读写同一文件时需要使用的文件锁功能。
模块函数
· fcntl.fcntl(fd, cmd, arg=0)
· fcntl.ioctl(fd, request, arg=0, mutate_flag=True)
· fcntl.flock(fd, operation)
· fcntl.lockf(fd, cmd, len=0, start=0, whence=0)
自己暂时只用到了 fcntl.flock
这个函数对文件上锁,在此做一些记录。
使用
考虑两个进程对同一个文件进行读写操作:
目录下有一个test.json
文件内容如下:
{
"test1": {
"1000": {
"files": "5"
},
"1001": {
"files": "6"
}
},
"test2": {
"2000": {
"files": "0"
},
"2001": {
"files": "1"
}
}
}
处理代码如下,模拟两个进程对该文件进行写操作:
import json
import time
import fcntl
import multiprocessing as mp
test_dict = {
"test1": {
"1001": {
"files": "6"
}
},
"test2": {
"2000": {
"files": "0"
},
"2001": {
"files": "1"
}
}
}
def test1(json_path):
with open(json_path, 'r+') as test_file:
fcntl.flock(test_file.fileno(), fcntl.LOCK_EX) #加锁,with块外自动释放锁
json_content = test_dict
# time.sleep(5)
test_file.truncate()#手动清空
json.dump(json_content, test_file, sort_keys=True, indent=4, separators=(',', ': '))
def test2(json_path):
with open(json_path, 'r+') as test_file:
fcntl.flock(test_file.fileno(), fcntl.LOCK_EX)
test_dict['test1'][str(3001)] = {"files":str(7)}
json_content = test_dict
test_file.truncate()
json.dump(json_content, test_file, sort_keys=True, indent=4, separators=(',', ': '))
if __name__ == '__main__':
json_path = r'./test.json'
p = mp.Process(target=test1, args=(json_path,))
q = mp.Process(target=test2, args=(json_path,))
p.start()
q.start()
p.join()
q.join()
分析:
进程p
先启动后获取到文件锁,以r+
模式打开test.json
文件,使用with...open...
打开时文件内容不会清空,所以在多进程读写同一文件时需要特别注意文件的打开方式。在写入前使用truncate()
函数手动清空内容,最终文件内容如下:
{
"test1": {
"1001": {
"files": "6"
}
},
"test2": {
"2000": {
"files": "0"
},
"2001": {
"files": "1"
}
}
}
进程q
后启动,在with...open...
后希望获取文件锁,此时如果进程p
打开的文件还未处理结束,文件锁还未释放之前,q
会一直处于阻塞状态。直到进程p
的with...open...
代码块结束后文件锁自动释放(也可以选择手动释放文件锁),进程q
才获取到文件锁,开始往test.json
文件中写入内容。最终文件内容如下:
{
"test1": {
"1001": {
"files": "6"
},
"3001": {
"files": "7"
},
},
"test2": {
"2000": {
"files": "0"
},
"2001": {
"files": "1"
}
}
}
两个进程读写同一文件,假设进程p
先启动先获得文件锁,进程q
后启动后获得文件锁:
进程p | 进程q | 说明 |
---|---|---|
读 | 写 | p读到旧文件内容后释放锁,q获得锁后写新内容到文件 |
读 | 读 | 不涉及文件内容修改,均读到同样的文件内容 |
写 | 读 | p写新内容到文件后释放锁,q获得锁后读到新文件内容 |
写 | 写 | p写新内容到文件后释放锁,q获得锁后写新内容到文件 |
多进程读写同一文件时均需要考虑加锁,否则会出现异常情况。例如在进程p
获得文件锁后正在写文件,进程q
不考虑加锁直接读取该文件,可能会由于文件内容被进程p
清空(包括“以w模式打开时清空”和“以truncate()方式清空”)导致读取出错。当读取操作也加锁时,如果进程p
未结束文件锁未被释放,那么进程q
将一直等待,直到得到锁后读取文件,这时不会出错。
总结:
(1)在打开一个文件时,以w模式和以r+模式打开时表现不同。其中以w模式打开时会清空文件内容,写入时是将新内容写入文件;以r+模式打开时不会清空文件内容,写入时是从游标0处写入新内容,但旧内容如果长于新内容,则会出现未被覆盖掉的旧内容,导致内容出错。需要特别注意。
(2)文件锁不要长期占用,处理结束后尽快释放掉。
(3)多进程读写同一文件时避免以w(以及w+等)模式打开文件,否则打开瞬间文件内容即被清空。
(4)加文件锁写json的代码:
def server_json_dump(json_content, json_path):
"""写json"""
if os.path.exists(json_path):
mode = 'r+'#以r+模式打开避免with...open打开文件时自动清空内容同时被读取
else:
mode = 'w'#以w模式打开以新建文件
with open(json_path, mode=mode) as result_file:#以r+形式打开
fcntl.flock(result_file.fileno(), fcntl.LOCK_EX) #加锁,with块外自动释放锁
result_file.truncate()#手动清空,避免r+模式下文本重叠
json.dump(json_content, result_file, sort_keys=True, indent=4, separators=(',', ': '))
参考资料
[1] fcntl — The fcntl and ioctl system calls
[2] Python 标准库之 fcntl
[3] Python open函数
[4] Python清空文件并替换内容
[5] 文件锁 python 进程间锁 fcntl