Threadsafe и отказоустойчивые записи файлов
У меня есть длительный процесс, который много пишет в файл. Результат должен быть все или ничего, поэтому я пишу во временный файл и переименовать его в имя в конце. В настоящее время мой код выглядит так:
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)
Я не доволен по нескольким причинам:
- он не очищается должным образом, если происходит исключение
- он игнорирует проблемы параллелизма
- это не многоразовые (мне нужно это в разных местах в моей программе)
любые предложения, как улучшить мой код? Есть библиотека, которая может мне помочь?
4 ответов
вы можете использовать Python в tempfile
модуль, чтобы дать вам имя временного файла. Он может создать временный файл потокобезопасным способом, а не создавать его с помощью time.time()
который может возвращать одно и то же имя, если используется в нескольких потоках одновременно.
как предложено в комментарии к вашему вопросу, это может быть связано с использованием контекстного менеджера. Вы можете получить некоторые идеи о том, как реализовать то, что вы хотите сделать, посмотрев на Python tempfile.py
источники.
следующий фрагмент кода может делать то, что вы хотите. Он использует некоторые из внутренних объектов, возвращенных из tempfile
.
- создание временных файлов является потокобезопасным.
- переименование файлов после успешного завершения является атомарным, по крайней мере, в Linux. Нет отдельной проверки между
os.path.exists()
иos.rename()
который может ввести условие гонки. Для атомарного переименования в Linux источник и назначения должны быть одинаковыми файловая система, поэтому этот код помещает временный файл в тот же каталог, что и целевой файл. - на
RenamedTemporaryFile
класс должен вести себя какNamedTemporaryFile
для большинства целей, за исключением случаев, когда он закрыт с помощью context manager, файл переименовывается.
пример:
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
вы можете использовать его следующим образом:
with RenamedTemporaryFile('whatever') as f:
f.write('stuff')
во время записи содержимое переходит во временный файл, при выходе файл переименовывается. Этот код, вероятно, будет нужны некоторые настройки, но общая идея должна помочь вам начать работу.
записать все или ничего в файл надежно:
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)
см. также является ли rename () без fsync() безопасным? (упоминается @Mihai Stan)
использование
with FaultTolerantFile('very_important_file') as file:
file.write('either all ')
file.write('or nothing is written')
для реализации отсутствует os.replace()
можно назвать MoveFileExW(src, dst, MOVEFILE_REPLACE_EXISTING)
(через модули win32file или ctypes) в Windows.
в случае нескольких потоков вы можете вызвать queue.put(data)
от
различные темы и писать в файл в выделенная тема:
for data in iter(queue.get, None):
file.write(data)
queue.put(None)
разрыв петли.
в качестве альтернативы вы можете использовать блокировки (резьба, многопроцессорная обработка, забыл) для синхронизации доступа:
def write(self, data):
with self.lock:
self.file.write(data)
можно использовать модуль lockfile чтобы заблокировать файл во время записи в него. Любая последующая попытка заблокировать его будет блокироваться до тех пор, пока блокировка от предыдущего процесса/потока не будет освобождена.
from lockfile import FileLock
with FileLock(filename):
#open your file here....
таким образом, вы обходите свои проблемы параллелизма и не должны очищать любой оставшийся файл, если происходит исключение.
на with
construct полезен для очистки при выходе, но не для системы фиксации/отката, которую вы хотите. А попробуйте / кроме / else блок можно использовать для этого.
вы также должны использовать стандартный способ создания временного имени файла, например, с помощью tempfile модуль.
и не забудьте fsync перед переименованием
Ниже приведен полный измененный код:
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)