Python 线程安全和容错文件写入
我有一个长时间运行的过程,在一个文件中写很多东西。结果应该是全部或全部,所以我正在写入一个临时文件,并在最后将其重命名为真实名称。目前,我的代码如下:Python 线程安全和容错文件写入,python,file,thread-safety,Python,File,Thread Safety,我有一个长时间运行的过程,在一个文件中写很多东西。结果应该是全部或全部,所以我正在写入一个临时文件,并在最后将其重命名为真实名称。目前,我的代码如下: 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):
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)
with RenamedTemporaryFile('whatever') as f:
f.write('stuff')
我对此不满意,原因有几个:
- 如果发生异常,它不会正确清理
- 它忽略了并发性问题
- 它是不可重用的(我需要在我的程序中的不同位置使用它)
from lockfile import FileLock
with FileLock(filename):
#open your file here....
这样,您就可以避免并发问题,并且在出现异常时不必清理任何遗留文件。您可以使用Python的
tempfile
模块为您提供一个临时文件名。它可以以线程安全的方式创建临时文件,而不是使用time.time()
创建临时文件,如果在多个线程中同时使用,该文件可能返回相同的名称
正如在对您的问题的评论中所建议的,这可以与使用上下文管理器相结合。通过查看Pythontempfile.py
源代码,您可以了解如何实现您想要做的事情
下面的代码段可以执行您想要的操作。它使用从tempfile
返回的一些对象的内部结构
- 创建临时文件是线程安全的
- 成功完成后重命名文件是原子的,至少在Linux上是这样。在
和os.path.exists()
之间没有单独的检查,这可能会引入竞争条件。对于Linux上的原子重命名,源文件和目标文件必须位于同一文件系统上,这就是为什么此代码将临时文件放置在与目标文件相同的目录中os.rename()
- 在大多数情况下,
类的行为应类似于RenamedTemporaryFile
,除非使用上下文管理器关闭该类,否则会重命名该文件NamedTemporaryFile
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
然后您可以这样使用它:
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)
with RenamedTemporaryFile('whatever') as f:
f.write('stuff')
在写入过程中,内容将转到临时文件,退出时文件将被重命名。这段代码可能需要一些调整,但总体思路应该有助于您开始。带有的
结构对于退出时的清理非常有用,但对于您想要的提交/回滚系统则不适用。可以使用try/except/else块进行此操作
您还应该使用标准方法创建临时文件名,例如使用tempfile模块
记住
以下是完整的修改代码:
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)
要可靠地将全部或全部写入文件,请执行以下操作:
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)
另见(所提及)
用法
要实现missing,可以在Windows上调用(通过Win32 File或ctypes模块)
如果有多个线程,您可以从中调用queue.put(data)
不同线程,并在专用线程中写入文件:
for data in iter(queue.get, None):
file.write(data)
queue.put(无)
中断循环
作为替代,您可以使用锁(线程、多处理、,
要同步访问,请执行以下操作:
def write(self, data):
with self.lock:
self.file.write(data)
为了使其可重用,您可以将其转换为上下文管理器。+1表示NamedTemporaryFile和上下文管理器。注意:如果不调用os.fsync()
。请参阅@J.F.Sebastian使用os.fsync()
可能会让您更接近容错系统,但fsync()
仍然不能保证数据在磁盘上,特别是对于具有写缓存的存储。我可能会根据应用程序选择使用/不使用fsync()
。有一些有趣的信息。飞利浦:OP专门询问容错,而不是最快的方法。但是大多数实现都做了一些有用的事情。在您的链接中:“某些文件系统可能会对元数据写入进行排序,以便将重命名写入磁盘,但新文件的内容尚未在磁盘上。如果我们在此时崩溃,则会在装载时检测到这一点,并将文件截断为0字节。调用fsync()可确保不会发生这种情况。[ext4]”。尽管在rename()中
case。谢谢,这很有帮助!J.F.是对的,在这种情况下,我不太关心性能,所以一个额外的函数调用就可以了。fsync
链接+1。注意:在os.fsync(f)
之前,您可能需要f.flush()
+1锁定文件链接。注意:对于同一进程中的线程,线程。可以使用Lock
(或多处理。Lock
用于通过多处理模块创建的处理)。谢谢,该模块看起来非常有用。谢谢,非常有用的东西。一些需要学习的新概念(试图让我了解@contextmanager
是如何工作的)。@thg435:yield
语句的目的/优点是什么?@Batman:contextmanager装饰师。阅读its文档以了解原因。然后单击我之前评论中的链接