Блокировка файла в Python

Мне нужно заблокировать файл для записи в Python. Он будет доступен сразу из нескольких процессов Python. Я нашел некоторые решения в сети, но большинство из них не подходят для моих целей, поскольку они часто основаны только на Unix или Windows.

15 ответов

Решение

Хорошо, так что я закончил тем, что пошел с кодом, который я написал здесь, на моем сайте ссылка не работает, просмотр на archive.org ( также доступен на GitHub). Я могу использовать его следующим образом:

from filelock import FileLock

with FileLock("myfile.txt"):
    # work with the file as it is now locked
    print("Lock acquired.")

Другие решения ссылаются на множество внешних кодовых баз. Если вы предпочитаете сделать это самостоятельно, вот код для кроссплатформенного решения, использующего соответствующие инструменты блокировки файлов в системах Linux / DOS.

try:
    # Posix based file locking (Linux, Ubuntu, MacOS, etc.)
    import fcntl
    def lock_file(f):
        fcntl.lockf(f, fcntl.LOCK_EX)
    def unlock_file(f): pass
except ModuleNotFoundError:
    # Windows file locking
    import msvcrt
    def file_size(f):
        return os.path.getsize( os.path.realpath(f.name) )
    def lock_file(f):
        msvcrt.locking(f.fileno(), msvcrt.LK_RLCK, file_size(f))
    def unlock_file(f):
        msvcrt.locking(f.fileno(), msvcrt.LK_UNLCK, file_size(f))


# Class for ensuring that all file operations are atomic, treat
# initialization like a standard call to 'open' that happens to be atomic
class AtomicOpen:
    # Open the file with arguments provided by user. Then acquire
    # a lock on that file object (WARNING: Advisory locking)
    def __init__(self, path, *args, **kwargs):
        # Open the file and acquire a lock on the file before operating
        self.file = open(path,*args, **kwargs)
        # Lock the opened file
        lock_file(self.file)

    # Return the opened file object (knowing a lock has been obtained)
    def __enter__(self, *args, **kwargs): return self.file

    # Allows users to use the 'close' function if they want, in case
    # the user did not have the AtomicOpen in a "with" block.
    def close(self): self.__exit__()

    # Unlock the file and close the file object
    def __exit__(self, exc_type=None, exc_value=None, traceback=None):        
        # Release the lock on the file
        unlock_file(self.file)
        self.file.close()
        # Handle exceptions that may have come up during execution, by
        # default any exceptions are raised to the user
        if (exc_type != None): return False
        else:                  return True        

Теперь "AtomicOpen" можно использовать везде, где обычно используется "открытый" оператор.

ВНИМАНИЕ: Если в Windows и Python происходит сбой перед вызовом exit, я не уверен, каково будет поведение блокировки.

ВНИМАНИЕ: Блокировка, представленная здесь, носит рекомендательный, а не абсолютный характер. Все потенциально конкурирующие процессы должны использовать класс "AtomicOpen".

Здесь есть кроссплатформенный модуль блокировки файлов: Portalocker

Хотя, как говорит Кевин, запись в файл из нескольких процессов одновременно - это то, чего вы хотите избежать, если это вообще возможно.

Если вы можете поместить вашу проблему в базу данных, вы можете использовать SQLite. Он поддерживает одновременный доступ и обрабатывает собственную блокировку.

Я искал несколько решений, чтобы сделать это, и мой выбор был oslo.concurrency

Это мощный и относительно хорошо документированный. Он основан на крепежных деталях.

Другие решения:

  • Portalocker: требуется pywin32, которая является exe-установкой, поэтому невозможна через pip
  • крепеж: плохо документирован
  • файл блокировки: устарел
  • flufl.lock: NFS-безопасная блокировка файлов для систем POSIX.
  • simpleflock: последнее обновление 2013-07
  • zc.lockfile: последнее обновление 2016-06 (по состоянию на 2017-03)
  • lock_file: последнее обновление 2007-10

Я предпочитаю lockfile - Платформо-независимая блокировка файлов

Блокировка зависит от платформы и устройства, но обычно у вас есть несколько вариантов:

  1. Используйте flock() или эквивалентный (если ваша ОС поддерживает это). Это рекомендательная блокировка, если вы не проверите ее, она игнорируется.
  2. Используйте методологию lock-copy-move-unlock, в которой вы копируете файл, записываете новые данные, затем перемещаете их (перемещаете, а не копируете - перемещение является атомарной операцией в Linux - проверьте вашу ОС), и вы проверяете наличие наличие файла блокировки.
  3. Используйте каталог как "замок". Это необходимо, если вы пишете в NFS, так как NFS не поддерживает flock().
  4. Существует также возможность использования общей памяти между процессами, но я никогда не пробовал этого; это очень зависит от ОС.

Для всех этих методов вам придется использовать технику спин-блокировки (повторная попытка отказа) для получения и тестирования блокировки. Это оставляет маленькое окно для неправильной синхронизации, но обычно оно достаточно маленькое, чтобы не быть основной проблемой.

Если вы ищете решение, которое является кроссплатформенным, то вам лучше подключиться к другой системе через какой-то другой механизм (следующая лучшая вещь - это метод NFS, описанный выше).

Обратите внимание, что на sqlite распространяются те же ограничения на NFS, что и на обычные файлы, поэтому вы не можете писать в базу данных sqlite на сетевом ресурсе и получать синхронизацию бесплатно.

Чтобы добавить к ответу Эвана Фоссмарка, вот пример того, как использовать файловую блокировку:

from filelock import FileLock

lockfile = r"c:\scr.txt"
lock = FileLock(lockfile + ".lock")
with lock:
    file = open(path, "w")
    file.write("123")
    file.close()

Любой код внутри with lock: block является потокобезопасным, что означает, что он будет завершен до того, как другой процесс получит доступ к файлу.

Координация доступа к одному файлу на уровне ОС чревата всевозможными проблемами, которые вы, вероятно, не хотите решать.

Лучше всего иметь отдельный процесс, который координирует доступ на чтение / запись к этому файлу.

Блокировка файла обычно является операцией, зависящей от платформы, поэтому вам может потребоваться возможность запуска в разных операционных системах. Например:

import os

def my_lock(f):
    if os.name == "posix":
        # Unix or OS X specific locking here
    elif os.name == "nt":
        # Windows specific locking here
    else:
        print "Unknown operating system, lock unavailable"

это сработало для меня: не занимать большие файлы, распределять по нескольким маленьким, вы создаете файл Temp, удаляете файл A, а затем переименовываете файл Temp в A.

      import os
import json

def Server():
    i = 0
    while i == 0:
        try:        
                with open(File_Temp, "w") as file:
                    json.dump(DATA, file, indent=2)
                if os.path.exists(File_A):
                    os.remove(File_A)
                os.rename(File_Temp, File_A)
                i = 1
        except OSError as e:
                print ("file locked: " ,str(e))
                time.sleep(1)
            
            
def Clients():
    i = 0
    while i == 0:
        try:
            if os.path.exists(File_A):
                with open(File_A,"r") as file:
                    DATA_Temp = file.read()
            DATA = json.loads(DATA_Temp)
            i = 1
        except OSError as e:
            print (str(e))
            time.sleep(1)

Я работал над такой ситуацией, когда я запускаю несколько копий одной и той же программы из одного каталога / папки и регистрирую ошибки. Мой подход состоял в том, чтобы записать "файл блокировки" на диск перед открытием файла журнала. Программа проверяет наличие "файла блокировки", прежде чем продолжить, и ждет его очереди, если "файл блокировки" существует.

Вот код:

def errlogger(error):

    while True:
        if not exists('errloglock'):
            lock = open('errloglock', 'w')
            if exists('errorlog'): log = open('errorlog', 'a')
            else: log = open('errorlog', 'w')
            log.write(str(datetime.utcnow())[0:-7] + ' ' + error + '\n')
            log.close()
            remove('errloglock')
            return
        else:
            check = stat('errloglock')
            if time() - check.st_ctime > 0.01: remove('errloglock')
            print('waiting my turn')

РЕДАКТИРОВАТЬ --- Подумав над комментариями о устаревших блокировках, приведенных выше, я отредактировал код, добавив проверку на устаревание "файла блокировки". Время выполнения нескольких тысяч итераций этой функции в моей системе составило в среднем 0,002066... секунд по сравнению с предыдущим:

lock = open('errloglock', 'w')

сразу после:

remove('errloglock')

поэтому я подумал, что начну с 5-кратной суммы, чтобы указать на устаревание и отслеживать ситуацию на предмет проблем.

Кроме того, когда я работал с синхронизацией, я понял, что у меня есть немного кода, который не был действительно необходим:

lock.close()

который у меня был сразу после оператора open, поэтому я удалил его в этом редактировании.

Сценарий таков: пользователь запрашивает файл, чтобы что-то сделать. Затем, если пользователь снова отправляет тот же запрос, он информирует пользователя о том, что второй запрос не выполнен, пока первый запрос не завершится. Вот почему я использую механизм блокировки для решения этой проблемы.

Вот мой рабочий код:

from lockfile import LockFile
lock = LockFile(lock_file_path)
status = ""
if not lock.is_locked():
    lock.acquire()
    status = lock.path + ' is locked.'
    print status
else:
    status = lock.path + " is already locked."
    print status

return status

Вы можете найти pylocker очень полезным. Его можно использовать для блокировки файла или для механизмов блокировки в целом, и к нему можно получить доступ сразу из нескольких процессов Python.

Если вы просто хотите заблокировать файл, вот как это работает:

import uuid
from pylocker import Locker

#  create a unique lock pass. This can be any string.
lpass = str(uuid.uuid1())

# create locker instance.
FL = Locker(filePath='myfile.txt', lockPass=lpass, mode='w')

# aquire the lock
with FL as r:
    # get the result
    acquired, code, fd  = r

    # check if aquired.
    if fd is not None:
        print fd
        fd.write("I have succesfuly aquired the lock !")

# no need to release anything or to close the file descriptor, 
# with statement takes care of that. let's print fd and verify that.
print fd

Я нашел простую и отработанную (!) Реализацию из grizzled-python.

Простое использование os.open(..., O_EXCL) + os.close() не работает на окнах.

Если вам просто нужен Mac/POSIX, это должно работать без внешних пакетов.

      import sys
import stat
import os


filePath = "<PATH TO FILE>"
if sys.platform == 'darwin':
  flags = os.stat(filePath).st_flags
  if flags & ~stat.UF_IMMUTABLE:
    os.chflags(filePath, flags & stat.UF_IMMUTABLE)

и если вы хотите разблокировать файл, просто измените его,

        if flags & stat.UF_IMMUTABLE:
    os.chflags(filePath, flags & ~stat.UF_IMMUTABLE)
Другие вопросы по тегам