Но функция открытия вроде как очищает файл
Да, открытие файла в режиме записи w
всегда очищает файл;из open()
документации по функциям :
'w'
открыть для записи, сначала обрезать файл
[...] По умолчаниюрежим 'r'
(открыт для чтения текста, синоним 'rt'
).Для двоичного доступа на чтение и запись, режим 'w+b'
открывает и усекает файл до 0 байтов.'r+b'
открывает файл без усечения.
Вы хотите заблокировать файл до его усечения .Вы также можете открыть файл в режиме 'r+'
(чтение и запись), после чего вам необходимо вручную обрезать его после блокировки.
Вам также потребуется заблокировать файл для чтения, потому что вы не хотите, чтобы ваши читатели заканчивали усеченными данными, когда они пытаются читать, пока другой процесс занят заменой содержимого.Используйте разделяемую блокировку, после чего другим процессам разрешается получать разделяемую блокировку тоже , что позволяет многим процессам читать данные без необходимости ждать друг друга.Процесс, который хочет написать, должен получить эксклюзивную блокировку, которая будет присваиваться только тогда, когда больше нет общих блокировок.
Лично я бы создал менеджер контекста, который обрабатывает блокировку (либо вэксклюзивный режим для записи или в режиме совместного использования для чтения), и обрезать файл только после получения блокировки.Вам также нужно будет учесть, что файл еще не существует, и если вы не хотите ждать блокировок вечно, вам нужно обрабатывать таймауты (то есть вам нужно использовать LOCK_NB
в цикле и проверьте возвращаемое значение, чтобы увидеть, была ли получена блокировка, до истечения определенного времени).
В следующем диспетчере контекста я использовал системный вызов низкого уровня os.open()
, чтобы гарантироватьфайл создается при попытке заблокировать его для монопольного доступа без его усечения, если он уже существует :
import errno
import fcntl
import os
import time
class Timeout(Exception):
"""Could not obtain a lock within the time given"""
class LockedFile:
"""Lock and open a file.
If the file is opened for writing, an exclusive lock is used,
otherwise it is a shared lock
"""
def __init__(self, path, mode, timeout=None, **fileopts):
self.path = path
self.mode = mode
self.fileopts = fileopts
self.timeout = timeout
# lock in exclusive mode when writing or appending (including r+)
self._exclusive = set('wa+').intersection(mode)
self._lockfh = None
self._file = None
def _acquire(self):
if self._exclusive:
# open the file in write & create mode, but *without the
# truncate flag* to make sure it is created only if it
# doesn't exist yet
lockfhmode, lockmode = os.O_WRONLY | os.O_CREAT, fcntl.LOCK_EX
else:
lockfhmode, lockmode = os.O_RDONLY, fcntl.LOCK_SH
self._lockfh = os.open(self.path, lockfhmode)
start = time.time()
while True:
try:
fcntl.lockf(self._lockfh, lockmode | fcntl.LOCK_NB)
return
except OSError as e:
if e.errno not in {errno.EACCES, errno.EAGAIN}:
raise
if self.timeout is not None and time.time() - start > self.timeout:
raise Timeout()
time.sleep(0.1)
def _release(self):
fcntl.lockf(self._lockfh, fcntl.LOCK_UN)
os.close(self._lockfh)
def __enter__(self):
if self._file is not None:
raise LockException('Lock already taken')
self._acquire()
self._file = open(self.path, self.mode, **self.fileopts)
return self._file
def __exit__(self, *exc):
if self._file is None:
raise LockException('Not locked')
self._file.close()
self._file = None
self._release()
Процессы, которые пытаются прочитать файл, затем используют:
with LockedFile('test.json', 'r') as file:
data = json.load(file)
и процесс, который хочет записать, использует:
with LockedFile('test.json', 'w') as file:
json.dump(data, file)
Если вы хотите разрешить тайм-аут, добавьте блок try/except
вокруг блока with
и поймайте исключение Timeout
;вам нужно решить, что должно произойти:
try:
with LockedFile('test.json', 'w', timeout=10) as file:
json.dump(data, file)
except Timeout:
# could not acquire an exclusive lock to write the file. What now?