Каков наилучший способ открыть файл для эксклюзивного доступа в Python? - PullRequest
38 голосов
/ 09 октября 2008

Какой самый элегантный способ решить эту проблему:

  • открыть файл для чтения, но только если он еще не открыт для записи
  • открыть файл для записи, но только если он еще не открыт для чтения или записи

Встроенные функции работают следующим образом

>>> path = r"c:\scr.txt"
>>> file1 = open(path, "w")
>>> print file1
<open file 'c:\scr.txt', mode 'w' at 0x019F88D8>
>>> file2 = open(path, "w")
>>> print file2
<open file 'c:\scr.txt', mode 'w' at 0x02332188>
>>> file1.write("111")
>>> file2.write("222")
>>> file1.close()

scr.txt теперь содержит «111».

>>> file2.close()

scr.txt был перезаписан и теперь содержит «222» (в Windows, Python 2.4).

Решение должно работать внутри того же процесса (как в примере выше), а также когда другой процесс открыл файл.
Желательно, если сбойная программа не будет держать блокировку открытой.

Ответы [ 6 ]

23 голосов
/ 09 октября 2008

Я не думаю, что есть полностью кроссплатформенный способ. В Unix модуль fcntl сделает это за вас. Однако в Windows (я предполагаю, что вы находитесь по путям) вам нужно использовать модуль win32file.

К счастью, есть переносимая реализация ( portalocker ), использующая соответствующий метод платформы в поваренной книге python.

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

portalocker.lock(file, flags)

где флаги - portalocker.LOCK_EX для эксклюзивного доступа на запись или LOCK_SH для общего доступа на чтение.

9 голосов
/ 12 октября 2008

Решение должно работать внутри того же процесса (как в примере выше), а также когда другой процесс открыл файл.

Если под «другим процессом» вы подразумеваете «любой процесс» (т. Е. Не ваша программа), в Linux нет способа выполнить это, полагаясь только на системные вызовы ( fcntl и друзья). Вам нужно обязательное блокирование , и способ Linux получить его немного сложнее:

Перемонтируйте раздел, содержащий ваш файл, с опцией mand :

# mount -o remount,mand /dev/hdXY

Установите флаг sgid для вашего файла:

# chmod g-x,g+s yourfile

В вашем коде Python получите эксклюзивную блокировку для этого файла:

fcntl.flock(fd, fcntl.LOCK_EX)

Теперь даже cat не сможет прочитать файл, пока вы не снимите блокировку.

3 голосов
/ 30 января 2014

Предполагая, что ваш интерпретатор Python, а также лежащая в его основе ОС и файловая система рассматривают os.rename как атомарную операцию, и при наличии получателя произойдет ошибка, следующий метод свободен от условий гонки. Я использую это в производстве на машине Linux. Не требует никаких сторонних библиотек и не зависит от ОС, кроме создания дополнительного файла, снижение производительности является приемлемым для многих случаев использования. Вы можете легко применить шаблон декоратора функций Python или контекстный менеджер with_statement, чтобы абстрагироваться от беспорядка.

Перед тем, как начать новый процесс / задачу, вам необходимо убедиться, что имя_файла lock_filename не существует.

import os,time
def get_tmp_file():
    filename='tmp_%s_%s'%(os.getpid(),time.time())
    open(filename).close()
    return filename

def do_exclusive_work():
    print 'exclusive work being done...'

num_tries=10
wait_time=10
lock_filename='filename.lock'
acquired=False
for try_num in xrange(num_tries):
    tmp_filename=get_tmp_file()
    if not os.path.exists(lock_filename):
        try:
            os.rename(tmp_filename,lock_filename)
            acquired=True
        except (OSError,ValueError,IOError), e:
            pass
    if acquired:
        try:
            do_exclusive_work()
        finally:
            os.remove(lock_filename)
        break
    os.remove(tmp_filename)
    time.sleep(wait_time)
assert acquired, 'maximum tries reached, failed to acquire lock file'

EDIT

Выяснилось, что os.rename молча перезаписывает назначение в ОС, отличной от Windows. Спасибо, что указали на это @ akrueger!

Вот обходной путь, собранный из здесь :

Вместо использования os.rename вы можете использовать:

try:
    if os.name != 'nt': # non-windows needs a create-exclusive operation
        fd = os.open(lock_filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
        os.close(fd)
    # non-windows os.rename will overwrite lock_filename silently.
    # We leave this call in here just so the tmp file is deleted but it could be refactored so the tmp file is never even generated for a non-windows OS
    os.rename(tmp_filename,lock_filename)
    acquired=True
except (OSError,ValueError,IOError), e:
    if os.name != 'nt' and not 'File exists' in str(e): raise

@ akrueger Вы, вероятно, в порядке с решением на основе каталогов, просто предлагая альтернативный метод.

3 голосов
/ 09 октября 2008

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

Требуется Python для расширений Windows , чтобы перейти к API win32, но это в значительной степени обязательно для Python на Windows уже, и в качестве альтернативы может быть сделано с ctypes . Код может быть адаптирован для предоставления большей функциональности, если это необходимо (например, разрешение FILE_SHARE_READ, а не совместное использование вообще). См. Также документацию MSDN для системных вызовов CreateFile и WriteFile и статью Создание и открытие файлов .

Как уже упоминалось, вы можете использовать стандартный модуль fcntl для реализации половины Unix этого, если требуется.

import winerror, pywintypes, win32file

class LockError(StandardError):
    pass

class WriteLockedFile(object):
    """
    Using win32 api to achieve something similar to file(path, 'wb')
    Could be adapted to handle other modes as well.
    """
    def __init__(self, path):
        try:
            self._handle = win32file.CreateFile(
                path,
                win32file.GENERIC_WRITE,
                0,
                None,
                win32file.OPEN_ALWAYS,
                win32file.FILE_ATTRIBUTE_NORMAL,
                None)
        except pywintypes.error, e:
            if e[0] == winerror.ERROR_SHARING_VIOLATION:
                raise LockError(e[2])
            raise
    def close(self):
        self._handle.close()
    def write(self, str):
        win32file.WriteFile(self._handle, str)

Вот как ведет себя ваш пример сверху:

>>> path = "C:\\scr.txt"
>>> file1 = WriteLockedFile(path)
>>> file2 = WriteLockedFile(path) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
    ...
LockError: ...
>>> file1.write("111")
>>> file1.close()
>>> print file(path).read()
111
2 голосов
/ 16 февраля 2015

РЕДАКТИРОВАТЬ: Я решил это сам! Используя существование каталога & age в качестве механизма блокировки! Блокировка по файлу безопасна только в Windows (потому что Linux молча перезаписывает), но блокировка по каталогу отлично работает как в Linux, так и в Windows. Посмотрите мой GIT, где я создал простой в использовании класс 'lockbydir.DLock' для этого:

https://github.com/drandreaskrueger/lockbydir

В нижней части файла readme вы найдете 3 GIT-плеера, где вы можете увидеть примеры кода, исполняемые вживую в вашем браузере! Довольно круто, не правда ли? : -)

Спасибо за внимание


Это был мой оригинальный вопрос:

Я хотел бы ответить на parity3 (https://meta.stackoverflow.com/users/1454536/parity3), но я не могу ни комментировать напрямую («Вы должны иметь 50 репутацию, чтобы комментировать»), ни я не вижу способа связаться с ним / ней напрямую. вы предлагаете мне дозвониться до него?

Мой вопрос:

Я реализовал нечто похожее на то, что parity3 предложил здесь в качестве ответа: https://stackoverflow.com/a/21444311/3693375 («Предполагая, что ваш интерпретатор Python и ...»)

И это прекрасно работает - на Windows. (Я использую его для реализации механизма блокировки, который работает с независимо запущенными процессами. https://github.com/drandreaskrueger/lockbyfile)

Но, кроме четности 3, в Linux это НЕ работает:

os.rename (src, dst)

Переименуйте файл или каталог src в dst. ... в Unix, если dst существует и это файл, он будет заменен без уведомления, если у пользователя есть разрешение. Операция может потерпеть неудачу на некоторых разновидностях Unix, если src и dst находятся на разных файловых системах. В случае успеха, переименование будет быть атомарной операцией (это требование POSIX). В Windows, если dst уже существует, будет вызвано OSError (https://docs.python.org/2/library/os.html#os.rename)

Проблема с тихой заменой. В линуксе «Если dst уже существует, OSError будет поднят» отлично подходит для моих целей. Но только на Windows, к сожалению.

Полагаю, пример parity3 по-прежнему работает большую часть времени из-за его условия if

if not os.path.exists(lock_filename):
    try:
        os.rename(tmp_filename,lock_filename)

Но тогда все это уже не атомарно.

Потому что условие if может быть истинным в двух параллельных процессах, и тогда оба будут переименованы, но только один из них выиграет гонку переименования. И никаких исключений не возникло (в Linux).

Есть предложения? Спасибо!

П.С .: Я знаю, что это неправильный путь, но мне не хватает альтернативы. ПОЖАЛУЙСТА, не наказывайте меня понижением моей репутации. Я много смотрел вокруг, чтобы решить это сам. Как PM пользователи здесь? И ме почему я не могу?

0 голосов
/ 09 октября 2008

Чтобы обезопасить себя при открытии файлов в одном приложении, вы можете попробовать что-то вроде этого:

import time
class ExclusiveFile(file):
    openFiles = {}
    fileLocks = []

    class FileNotExclusiveException(Exception):
        pass

    def __init__(self, *args):

        sMode = 'r'
        sFileName = args[0]
        try:
            sMode = args[1]
        except:
            pass
        while sFileName in ExclusiveFile.fileLocks:
            time.sleep(1)

        ExclusiveFile.fileLocks.append(sFileName)

        if not sFileName in ExclusiveFile.openFiles.keys() or (ExclusiveFile.openFiles[sFileName] == 'r' and sMode == 'r'):
            ExclusiveFile.openFiles[sFileName] = sMode
            try:
                file.__init__(self, sFileName, sMode)
            finally:
                ExclusiveFile.fileLocks.remove(sFileName)
         else:
            ExclusiveFile.fileLocks.remove(sFileName)
            raise self.FileNotExclusiveException(sFileName)

    def close(self):
        del ExclusiveFile.openFiles[self.name]
        file.close(self)

Таким образом, вы создаете подкласс класса file. Теперь просто сделайте:

>>> f = ExclusiveFile('/tmp/a.txt', 'r')
>>> f
<open file '/tmp/a.txt', mode 'r' at 0xb7d7cc8c>
>>> f1 = ExclusiveFile('/tmp/a.txt', 'r')
>>> f1
<open file '/tmp/a.txt', mode 'r' at 0xb7d7c814>
>>> f2 = ExclusiveFile('/tmp/a.txt', 'w') # can't open it for writing now
exclfile.FileNotExclusiveException: /tmp/a.txt

Если вы сначала откроете его в режиме 'w', он больше не будет открываться даже в режиме чтения, как вы и хотели ...

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...