Threadsafe and fault-tolerant file writes
我有一个长期运行的过程,在一个文件中写了很多东西。结果应该是全部或全部,所以我将写入一个临时文件,并在最后将其重命名为实名。目前,我的代码如下:
1 2 3 4 5 6 7 8 9 10 | filename = 'whatever' tmpname = 'whatever' + str(time.time()) with open(tmpname, 'wb') as fp: fp.write(stuff) fp.write(more stuff) if os.path.exists(filename): os.unlink(filename) os.rename(tmpname, filename) |
我对此不满意,原因有几个:
- 如果发生异常,它将无法正常清理
- 它忽略并发性问题
- 它是不可重用的(我的程序在不同的地方需要它)
有什么关于如何改进我的代码的建议吗?有图书馆能帮我吗?
您可以使用python的
正如对您的问题的评论中所建议的,这可以与上下文管理器的使用相结合。通过查看python
下面的代码段可以满足您的需要。它使用从
- 创建临时文件是线程安全的。
- 成功完成后重命名文件是原子的,至少在Linux上是如此。在
os.path.exists() 和os.rename() 之间没有单独的检查可以引入一个种族条件。对于Linux上的原子重命名,源文件和目标文件必须位于同一文件系统上,这就是为什么此代码将临时文件放在与目标文件相同的目录中的原因。 - 在大多数情况下,
RenamedTemporaryFile 类的行为应该类似于NamedTemporaryFile ,除非使用上下文管理器关闭该文件,否则将重命名该文件。
Sample:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import tempfile import os class RenamedTemporaryFile(object): """ A temporary file object which will be renamed to the specified path on exit. """ def __init__(self, final_path, **kwargs): tmpfile_dir = kwargs.pop('dir', None) # Put temporary file in the same directory as the location for the # final file so that an atomic move into place can occur. if tmpfile_dir is None: tmpfile_dir = os.path.dirname(final_path) self.tmpfile = tempfile.NamedTemporaryFile(dir=tmpfile_dir, **kwargs) self.final_path = final_path def __getattr__(self, attr): """ Delegate attribute access to the underlying temporary file object. """ return getattr(self.tmpfile, attr) def __enter__(self): self.tmpfile.__enter__() return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is None: self.tmpfile.delete = False result = self.tmpfile.__exit__(exc_type, exc_val, exc_tb) os.rename(self.tmpfile.name, self.final_path) else: result = self.tmpfile.__exit__(exc_type, exc_val, exc_tb) return result |
然后您可以这样使用它:
1 2 | with RenamedTemporaryFile('whatever') as f: f.write('stuff') |
在写入过程中,内容将转到一个临时文件,退出时该文件将被重命名。这段代码可能需要一些调整,但一般的想法应该可以帮助您开始。
可靠地将全部或全部内容写入文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | import os from contextlib import contextmanager from tempfile import NamedTemporaryFile if not hasattr(os, 'replace'): os.replace = os.rename #NOTE: it won't work for existing files on Windows @contextmanager def FaultTolerantFile(name): dirpath, filename = os.path.split(name) # use the same dir for os.rename() to work with NamedTemporaryFile(dir=dirpath, prefix=filename, suffix='.tmp') as f: yield f f.flush() # libc -> OS os.fsync(f) # OS -> disc (note: on OSX it is not enough) f.delete = False # don't delete tmp file if `replace()` fails f.close() os.replace(f.name, name) |
另请参见不带fsync()的rename()是否安全?(由@mihai stan提到)
用法1 2 3 | with FaultTolerantFile('very_important_file') as file: file.write('either all ') file.write('or nothing is written') |
要实现缺少的
如果有多个线程,可以从不同的线程并在专用线程中写入文件:
1 2 | for data in iter(queue.get, None): file.write(data) |
作为一种替代方法,您可以使用锁(线程、多处理、filelock)同步访问:
1 2 3 | def write(self, data): with self.lock: self.file.write(data) |
您还应该使用标准方法来创建临时文件名,例如使用tempfile模块。
记住在重命名之前进行fsync
以下是完整的修改代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import time, os, tempfile def begin_file(filepath): (filedir, filename) = os.path.split(filepath) tmpfilepath = tempfile.mktemp(prefix=filename+'_', dir=filedir) return open(os.path.join(filedir, tmpfilepath), 'wb') def commit_file(f): tmppath = f.name (filedir, tmpname) = os.path.split(tmppath) origpath = os.path.join(filedir,tmpname.split('_')[0]) os.fsync(f.fileno()) f.close() if os.path.exists(origpath): os.unlink(origpath) os.rename(tmppath, origpath) def rollback_file(f): tmppath = f.name f.close() os.unlink(tmppath) fp = begin_file('whatever') try: fp.write('stuff') except: rollback_file(fp) raise else: commit_file(fp) |
您可以在写入文件时使用lock file模块锁定文件。在释放前一个进程/线程的锁之前,任何随后的锁定尝试都将被阻止。
1 2 3 | from lockfile import FileLock with FileLock(filename): #open your file here.... |
这样,您就可以绕过并发性问题,并且在发生异常时不必清理任何剩余的文件。