关于python:Threadsafe和容错文件写入

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的tempfile模块为您提供一个临时文件名。它可以以线程安全的方式创建一个临时文件,而不是使用time.time()来组成一个临时文件,如果同时在多个线程中使用,则该文件可能返回相同的名称。

正如对您的问题的评论中所建议的,这可以与上下文管理器的使用相结合。通过查看python tempfile.py源,您可以了解如何实现您想要做的事情。

下面的代码段可以满足您的需要。它使用从tempfile返回的对象的一些内部结构。

  • 创建临时文件是线程安全的。
  • 成功完成后重命名文件是原子的,至少在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')

要实现缺少的os.replace(),您可以在Windows上调用MoveFileExW(src, dst, MOVEFILE_REPLACE_EXISTING)(通过win32file或ctypes模块)。

如果有多个线程,可以从不同的线程并在专用线程中写入文件:

1
2
 for data in iter(queue.get, None):
     file.write(data)

queue.put(None)中断了循环。

作为一种替代方法,您可以使用锁(线程、多处理、filelock)同步访问:

1
2
3
def write(self, data):
    with self.lock:
        self.file.write(data)


with构造对于在退出时清理非常有用,但对于您想要的提交/回滚系统则不有用。可以使用try/except/else块。

您还应该使用标准方法来创建临时文件名,例如使用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....

这样,您就可以绕过并发性问题,并且在发生异常时不必清理任何剩余的文件。