这种文件锁定方法可以接受吗?
我们有10台Linux电脑,每周需要完成100个不同的任务。这些电脑大部分时间在晚上工作,正好是我们在家的时候。我的一个同事正在做一个项目,想通过用Python自动启动任务来优化运行时间。他的程序会读取任务列表,选择一个空闲的任务,然后在文件中标记这个任务为“进行中”,等任务完成后再把它标记为“已完成”。这些任务文件会存放在我们的网络共享上。
我们知道,不建议多个程序同时访问同一个文件,但我们似乎没有其他选择。在他寻找防止两台电脑同时写入文件的方法时,我想出了一个自己的方法,感觉比网上找到的那些方法更容易实现。我的方法是先检查文件是否存在,如果不存在就等几秒钟,如果存在就暂时移动这个文件。我写了一个脚本来测试这个方法:
#!/usr/bin/env python
import time, os, shutil
from shutil import move
from os import path
fh = "testfile"
fhtemp = "testfiletemp"
while os.path.exists(fh) == False:
time.sleep(3)
move(fh, fhtemp)
f = open(fhtemp, 'w')
line = raw_input("type something: ")
print "writing to file"
f.write(line)
raw_input("hit enter to close file.")
f.close()
move(fhtemp, fh)
在我们的测试中,这个方法是有效的,但我在想,使用这个方法会不会有我们看不到的问题。我意识到,如果两台电脑同时运行exists(),可能会出现灾难性的后果。不过,由于任务的执行时间在20分钟到8小时之间,同时到达这个检查点的可能性不大。
8 个回答
在大多数操作系统中,文件的移动或重命名通常是一个原子操作,也就是说这个过程要么完全成功,要么完全失败,所以这个方法应该是可行的。
不过,你需要在你的 move
和 open
调用中添加异常检查,以防在你检查文件存在与移动文件之间,有其他进程把文件移动了(或者如果 move
没有成功完成)。
编辑
下面是一个有效的操作流程总结:
- 把文件从 A 移动到 A.[myID]
- 尝试打开 A.[myID]
- 如果第 1 步或第 2 步失败,说明我们没有获得锁,等一会儿再回到第 1 步。否则,说明我们获得了锁,可以继续。
- 进行修改。
- 把文件从 A.[myID] 移动回 A。(这一步应该永远不会失败。)这样就释放了锁。
一个不错的 [myID]
选择是进程的 PID(进程标识符),如果在多个系统上运行的话,可能还要包括主机信息。
看起来你在做一件其实可以很简单的事情,但你可能花了太多的精力。现在你有一个文件,里面列出了所有的任务。
不如把任务队列改成一个文件夹,每个待处理的任务都变成一个文件怎么样?这样做就简单多了,你只需要从“待处理”文件夹里挑一个任务,移动到“正在进行”文件夹,等任务完成后,再把这个文件移动到“已完成”文件夹。因为文件移动是一个原子操作,所以不会出现竞争问题(如果移动失败,那就说明另一个工作者先把它拿走了,你只需要挑下一个任务就行)。
而且,查看进度也很简单,只需要在其中一个文件夹里执行 ls
命令就可以了 :-)
你基本上是开发了一个文件系统版本的二进制信号量(或者说互斥锁)。这是一个经过充分研究的结构,主要用于锁定,所以只要你实现的细节没问题,它就应该能正常工作。关键在于要让“测试并设置”操作,或者在你的情况下是“检查是否存在并移动”,真正做到原子性。为此,我建议使用类似下面的代码:
lock_acquired = False
while not lock_acquired:
try:
move(fh, fhtemp)
except:
sleep(3)
else:
lock_acquired = True
# do your writing
move(fhtemp, fh)
lock_acquired = False
你之前的程序大部分时间都能正常工作,但正如提到的,如果在你检查文件是否存在和调用move
之间,另一个进程把文件移动了,那就可能会出现问题。我想你可以找到解决办法,但我个人建议还是使用一个经过充分测试的互斥锁算法。(我把上面的代码示例从安德鲁·塔能鲍姆的《现代操作系统》翻译过来,但可能在转换过程中引入了一些错误——这只是个提醒)
顺便提一下,Linux上open
函数的手册页提供了一个文件锁定的解决方案:
使用锁文件进行原子文件锁定的解决方案是,在同一个文件系统上创建一个唯一的文件(例如,包含主机名和进程ID),然后使用link(2)来创建一个指向锁文件的链接。如果link()返回0,说明锁定成功。否则,使用stat(2)检查这个唯一文件的链接计数是否增加到2,如果是,那锁定也成功。
要在Python中实现这个,你可以这样做:
# each instance of the process should have a different filename here
process_lockfile = '/path/to/hostname.pid.lock'
# all processes should have the same filename here
global_lockfile = '/path/to/lockfile'
# create the file if necessary (only once, at the beginning of each process)
with open(process_lockfile, 'w') as f:
f.write('\n') # or maybe write the hostname and pid
# now, each time you have to lock the file:
lock_acquired = False
while not lock_acquired:
try:
link(process_lockfile, global_lockfile)
except:
lock_acquired = (stat(process_lockfile).st_nlinks == 2)
else:
lock_acquired = True
# do your writing
unlink(global_lockfile)
lock_acquired = False