Блокування файлу в 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
Ще одну вдосконалену версію файлу файлів Evan можна знайти тут: github.com/ilastik/lazyflow/blob/master/lazyflow/utility/…
Стюарт Берг

3
OpenStack опублікував власну (ну, пропустити Монтанаро) реалізацію - pylockfile - Дуже схожий на ті, що згадувалися в попередніх коментарях, але все ж варто поглянути.
jweyrich

7
@jweyrich Openstacks pylockfile тепер застарілий. Рекомендується замість цього використовувати крепежні елементи або звільнення .
харбун

2
Ще одна подібна реалізація, яку я думаю: github.com/benediktschmitt/py-filelock
herry

39

Тут є модуль міжплатформового блокування файлів: Portalocker

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

Якщо ви можете ввімкнути свою проблему в базу даних, ви можете використовувати SQLite. Він підтримує одночасний доступ та обробляє власне блокування.


16
+1 - SQLite майже завжди є шлях до подібних ситуацій.
cdleary

2
Порталоккер вимагає розширень Python для Windows.
n611x007

2
@naxa є його варіант, який спирається лише на msvcrt та ctypes, див. roundup.hg.sourceforge.net/hgweb/roundup/roundup/file/tip/…
The Cat

@ n611x007 Portalocker щойно оновлений, тому він більше не потребує розширень для Windows :)
Wolph

2
SQLite підтримує паралельний доступ?
piotr

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 завершує роботу перед викликом виходу , я не впевнений, якою буде поведінка блокування.

ПОПЕРЕДЖЕННЯ: Заблоковані тут блоки є дорадчими, а не абсолютними. Усі потенційно конкуруючі процеси повинні використовувати клас "AtomicOpen".


unlock_fileфайл на linux не повинен дзвонити fcntlзнову з LOCK_UNпрапором?
eadmaster

Розблокування відбувається автоматично, коли файл-об’єкт закритий. Однак мене погана практика програмування не включала. Я оновив код і додав операцію розблокування fcntl!
Томас Люкс

У __exit__вас closeпоза замком після unlock_file. Я вважаю, що час виконання може стерти дані (тобто записувати) під час close. Я вважаю, що потрібно flushі fsyncпід блокуванням, щоб переконатися, що під час блокування не записуються додаткові дані поза блоком close.
Бенджамін Баньє

Дякуємо за виправлення! Я перевірив , що є можливість для умови гонки без flushі fsync. Я додав два рядки , ви запропонували перед викликом unlock. Я пройшов повторну перевірку, і, здається, стан гонки вирішено.
Томас Люкс

1
Єдине, що піде "неправильно" - це те, що до часу блокування файлу 1 його вміст буде врізано (вміст видалено). Ви можете перевірити це самостійно, додавши ще один код "відкрити" з "w" до коду, що вийшов вище, перед блокуванням. Однак це неминуче, тому що ви повинні відкрити файл перед тим, як заблокувати його. Для уточнення, "атомний" означає, що у файлі буде знаходитися лише законний вміст файлу. Це означає, що ви ніколи не отримаєте файл із вмістом із кількох конкуруючих процесів, змішаних разом.
Томас Люкс

15

Я віддаю перевагу lockfile - блокування файлів, незалежне від платформи


3
Ця бібліотека здається добре написаною, але немає механізму виявлення застарілих файлів блокування. Він відслідковує PID, який створив замок, тому має бути можливо сказати, чи цей процес все ще працює.
шербанг


@JanusTroelsen модуль pidlockfile не отримує блокування атомно.
шербанг

@sherbang Ви впевнені? Він відкриває файл блокування в режимі O_CREAT | O_EXCL.
mhsmith

2
Зауважте, що ця бібліотека витісняється
congusbongus

13

Я розглядав декілька рішень для цього, і мій вибір був звільненим

Це потужне і відносно добре документоване. В його основі лежить кріплення.

Інші рішення:

  • Portalocker : вимагає pywin32, який є установкою exe, тому неможливо через pip
  • кріплення : погано документовані
  • lockfile : застарілий
  • 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 () або еквівалент (якщо ваш os підтримує це). Це доцільне блокування, якщо ви не перевіряєте наявність блокування, його ігнорується.
  2. Використовуйте методологію блокування-копіювання-переміщення-розблокування, де ви копіюєте файл, записуєте нові дані, потім переміщуєте їх (переміщення, а не копіювання - переміщення - це атомна операція в 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

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

Ваша найкраща ставка - це окремий процес, який координує доступ для читання / запису до цього файлу.


19
"окремий процес, який координує доступ для читання / запису до цього файлу" - іншими словами, реалізуйте сервер бази даних :-)
Елі Бендерський,

1
Це насправді найкраща відповідь. Якщо сказати, що "використовувати сервер бази даних", надмірно спрощено, оскільки db не завжди буде правильним інструментом для роботи. Що робити, якщо це повинен бути звичайний текстовий файл? Хорошим рішенням може стати нерестування дочірнього процесу, а потім доступ до нього через названу трубу, unix-сокет або спільну пам'ять.
Брендон Крофорд

9
-1 тому що це просто FUD без пояснень. Блокування файлу для запису здається мені досить простою концепцією, що ОС пропонує такі функції, як 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')

EDIT --- Після роздумів над деякими коментарями щодо застарілих замків вище я відредагував код, щоб додати перевірку на несвіжість "файлу блокування". Час виконання декількох тисяч ітерацій цієї функції в моїй системі дало в середньому 0,002066 ... секунд за минулий час:

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

до одразу після:

remove('errloglock')

тож я подумав, що почну з 5 разів цієї суми, щоб вказати на несвіжість та стежити за ситуацією щодо проблем.

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

lock.close()

який я мав відразу після відкритої заяви, тому я її видалив у цій редакції.


2

Щоб додати відповідь Евана Фосмарка , ось приклад використання блокування файлів :

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

0

Я знайшов просту і відпрацьовану (!) Реалізацію від grizzled-python.

Просте використання os.open (..., O_EXCL) + os.close () не працює на Windows.


4
Опція O_EXCL не пов’язана з блокуванням
Сергій

0

Ви можете виявити пілокер дуже корисним. Він може використовуватися для блокування файлу або для механізмів блокування взагалі, і до нього можна отримати доступ з декількох процесів 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.