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

152

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

Эван Фосмарк
источник

Ответы:

115

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

from filelock import FileLock

with FileLock("myfile.txt.lock"):
    print("Lock acquired.")
    with open("myfile.txt"):
        # work with the file as it is now locked
Эван Фосмарк
источник
10
Как отмечается в комментарии к сообщению в блоге, это решение не является «идеальным», поскольку программа может завершиться таким образом, что блокировка останется на месте, и вам придется вручную удалить блокировку перед файлом снова становится доступным Однако, кроме этого, это все еще хорошее решение.
leetNightshade
3
Еще одна улучшенная версия FileLock Эвана может быть найдена здесь: github.com/ilastik/lazyflow/blob/master/lazyflow/utility/…
Стюарт Берг
3
OpenStack опубликовал свою собственную (ну, Skip Montanaro) реализацию - pylockfile - очень похожую на ту, что упоминалась в предыдущих комментариях, но все же стоит взглянуть.
jweyrich
7
@jweyrich Openstacks pylockfile больше не поддерживается. Вместо этого рекомендуется использовать крепеж или oslo.concurrency .
Harbun
2
Еще одна похожая реализация, я думаю: github.com/benediktschmitt/py-filelock
herry
39

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

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

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

Джон Фухи
источник
16
+1 - SQLite - почти всегда путь в подобных ситуациях.
cledary
2
Для этого Portalocker требуются расширения Python для Windows.
n611x007
2
@naxa, есть вариант, который опирается только на msvcrt и ctypes, см. roundup.hg.sourceforge.net/hgweb/roundup/roundup/file/tip/…
The Cat,
@ n611x007 Portalocker был только что обновлен, поэтому он больше не требует никаких расширений для Windows :)
Wolph
2
SQLite поддерживает одновременный доступ?
Петр
23

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

try:
    # Posix based file locking (Linux, Ubuntu, MacOS, etc.)
    import fcntl, os
    def lock_file(f):
        fcntl.lockf(f, fcntl.LOCK_EX)
    def unlock_file(f):
        fcntl.lockf(f, fcntl.LOCK_UN)
except ModuleNotFoundError:
    # Windows file locking
    import msvcrt, os
    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.
# This file opener *must* be used in a "with" block.
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

    # Unlock the file and close the file object.
    def __exit__(self, exc_type=None, exc_value=None, traceback=None):        
        # Flush to make sure all buffered contents are written to file.
        self.file.flush()
        os.fsync(self.file.fileno())
        # 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можно использовать в withблоке, где обычно используется openоператор.

ПРЕДУПРЕЖДЕНИЕ. Если запуск в Windows и Python завершается сбоем до вызова exit , я не уверен, каково будет поведение блокировки.

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

Томас Люкс
источник
unlock_fileфайл на linux не должен вызывать fcntlснова с LOCK_UNфлагом?
Eadmaster
Разблокировка происходит автоматически при закрытии файлового объекта. Тем не менее, это была плохая практика программирования - не включать ее. Я обновил код и добавил операцию разблокировки fcntl!
Томас Люкс
У __exit__вас closeза пределами замка после unlock_file. Я считаю, что среда выполнения может сбрасывать (т.е. записывать) данные во время close. Я полагаю, что нужно flushи fsyncпод замком убедиться, что никакие дополнительные данные не записываются за пределы замка во время close.
Бенджамин Банье
Спасибо за исправление! Я проверил , что есть возможность для условия гонки без flushи fsync. Я добавил две предложенные вами строки перед звонком unlock. Я повторно проверил, и состояние гонки, кажется, решено.
Томас Люкс
1
Единственное, что пойдет не так, это то, что к тому времени, когда процесс 1 заблокирует файл, его содержимое будет обрезано (содержимое стерто). Вы можете проверить это самостоятельно, добавив еще один файл «open» с «w» в код выше перед блокировкой. Это неизбежно, потому что вы должны открыть файл перед его блокировкой. Чтобы уточнить, «атомарный» в том смысле, что в файле будет найдено только законное содержимое файла. Это означает, что вы никогда не получите файл с содержимым нескольких конкурирующих процессов.
Томас Люкс
15

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

ferrdo
источник
3
Эта библиотека кажется хорошо написанной, но нет механизма для обнаружения устаревших файлов блокировки. Он отслеживает PID, который создал блокировку, поэтому должно быть возможно определить, выполняется ли этот процесс еще.
Шербанг
1
@sherbang: как насчет remove_existing_pidfile ?
Янус Троелсен
@JanusTroelsen модуль pidlockfile не получает блокировки атомарно.
шербанг
@sherbang Ты уверен? Он открывает файл блокировки в режиме O_CREAT | O_EXCL.
mhsmith
2
Обратите внимание, что эта библиотека была заменена и является частью github.com/harlowja/fasteners
congusbongus
13

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

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

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

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

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

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

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

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

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

Ричард Левассер
источник
4
Примечание. Перемещение / переименование не является атомарным в Win32. Ссылка: stackoverflow.com/questions/167414/…
sherbang
4
Новая заметка: os.renameтеперь в Win32 атомарный начиная с Python 3.3: bugs.python.org/issue8828
Ghostkeeper
7

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

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

Kevin
источник
19
«отдельный процесс, который координирует доступ на чтение / запись к этому файлу» - другими словами, реализуйте сервер базы данных :-)
Eli Bendersky
1
Это на самом деле лучший ответ. Просто сказать «использовать сервер базы данных» слишком упрощенно, так как БД не всегда будет подходящим инструментом для работы. Что делать, если это должен быть простой текстовый файл? Хорошим решением может быть создание дочернего процесса и последующий доступ к нему через именованный канал, сокет unix или разделяемую память.
Брендон Кроуфорд
9
-1 потому что это просто ФУД без объяснения причин. Для меня блокировка файла для записи кажется довольно простой концепцией: операционные системы предлагают такие функции, как flockэта. Подход «накатить свои собственные мьютексы и процесс демона для управления ими» кажется довольно экстремальным и сложным подходом для решения ... проблемы, о которой вы на самом деле не говорили нам, а просто пугающе предположили, что она существует.
Марк Амери
-1 по причинам, указанным @Mark Amery, а также за то, что они предложили необоснованное мнение о том, какие проблемы хочет решить ОП
Майкл Кребс
5

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

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"
Грег Хьюгилл
источник
7
Возможно, вы уже знаете это, но модуль платформы также доступен для получения информации о работающей платформе. platform.system (). docs.python.org/library/platform.html .
Monkut
2

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

Вот код:

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, поэтому я удалил его в этом редактировании.

белая борода
источник
2

Чтобы добавить ответ Эвана Фоссмарка , вот пример использования filelock :

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:блока является поточно-ориентированным, что означает, что он будет завершен до того, как другой процесс получит доступ к файлу.

Джош Коррейя
источник
1

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

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

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
Günay Gültekin
источник
0

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

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

SpeQ
источник
4
Опция O_EXCL не связана с блокировкой
Сергей,
0

Вы можете найти 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
Cobry
источник