Blokowanie pliku w Pythonie

152

Muszę zablokować plik do pisania w Pythonie. Będzie dostępny z wielu procesów Pythona jednocześnie. Znalazłem kilka rozwiązań online, ale większość z nich nie spełnia moich oczekiwań, ponieważ często są one oparte tylko na Uniksie lub Windowsie.

Evan Fosmark
źródło

Odpowiedzi:

115

W porządku, więc skończyłem z kodem, który napisałem tutaj, na mojej stronie link nie działa, zobacz na archive.org ( również na GitHub ). Mogę go używać w następujący sposób:

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
Evan Fosmark
źródło
10
Jak zauważył komentarz w poście na blogu, to rozwiązanie nie jest "idealne", ponieważ istnieje możliwość zakończenia programu w taki sposób, że blokada pozostaje na miejscu i trzeba ręcznie usunąć blokadę przed plikiem stanie się ponownie dostępny. Jednak pomijając to, jest to nadal dobre rozwiązanie.
leetNightshade
3
Kolejną ulepszoną wersję Evan's FileLock można znaleźć tutaj: github.com/ilastik/lazyflow/blob/master/lazyflow/utility/…
Stuart Berg,
3
OpenStack opublikował własną (no cóż, Skip Montanaro) implementację - plik pylockfile - bardzo podobny do tych wspomnianych w poprzednich komentarzach, ale nadal warto się przyjrzeć.
jweyrich
7
Plik pylockfile @jweyrich Openstacks jest teraz przestarzały. Zamiast tego zaleca się użycie elementów złącznych lub oslo.concurrency .
harbun
2
Wydaje
herry
39

W tym miejscu znajduje się wieloplatformowy moduł blokowania plików: Portalocker

Chociaż, jak mówi Kevin, zapisywanie do pliku z wielu procesów jednocześnie jest czymś, czego chcesz uniknąć, jeśli to w ogóle możliwe.

Jeśli możesz umieścić problem w bazie danych, możesz użyć SQLite. Obsługuje równoczesny dostęp i obsługuje własne blokowanie.

John Fouhy
źródło
16
+1 - SQLite jest prawie zawsze dobrym rozwiązaniem w tego typu sytuacjach.
cdleary
2
Portalocker wymaga do tego rozszerzenia Python dla systemu Windows.
n611x007
2
@naxa istnieje wariant, który opiera się tylko na msvcrt i ctypes, patrz roundup.hg.sourceforge.net/hgweb/roundup/roundup/file/tip/ ...
Shmil The Cat
@ n611x007 Portalocker został właśnie zaktualizowany, więc nie wymaga już żadnych rozszerzeń w systemie Windows :)
Wolph
2
SQLite obsługuje jednoczesny dostęp?
piotr
23

Inne rozwiązania przytaczają wiele zewnętrznych baz kodu. Jeśli wolisz zrobić to sam, oto kod dla rozwiązania wieloplatformowego, które wykorzystuje odpowiednie narzędzia do blokowania plików w systemach 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        

Teraz AtomicOpenmoże być używany w withbloku, w którym normalnie używałbyś openinstrukcji.

OSTRZEŻENIE: Jeśli uruchamianie w systemie Windows i Python ulega awarii przed wywołaniem wyjścia , nie jestem pewien, jakie byłoby zachowanie blokady.

OSTRZEŻENIE: Podana tu blokada jest zalecana, a nie bezwzględna. Wszystkie potencjalnie konkurujące procesy muszą używać klasy „AtomicOpen”.

Thomas Lux
źródło
unlock_fileplik w fcntlsystemie Linux nie powinien dzwonić ponownie z LOCK_UNflagą?
eadmaster
Odblokowanie następuje automatycznie po zamknięciu obiektu pliku. Jednak nie uwzględnienie tego było złą praktyką programistyczną z mojej strony. Zaktualizowałem kod i dodałem operację odblokowania fcntl!
Thomas Lux
W __exit__tobie closepoza zamkiem po unlock_file. Uważam, że środowisko wykonawcze może opróżniać (tj. Zapisywać) dane podczas close. Uważam, że należy flushi fsyncpod zamkiem, aby upewnić się, że żadne dodatkowe dane nie zostaną zapisane poza zamkiem podczas close.
Benjamin Bannier
Dziękuję za poprawienie mnie! I sprawdzeniu, że nie ma możliwości do wyścigu bez flushi fsync. Dodałem dwie linie, które zasugerowałeś, zanim zadzwonisz unlock. Ponownie przetestowałem i stan wyścigu wydaje się być rozwiązany.
Thomas Lux,
1
Jedyną rzeczą, która pójdzie „źle”, jest to, że zanim proces 1 zablokuje plik, jego zawartość zostanie obcięta (zawartość zostanie usunięta). Możesz to sprawdzić samodzielnie, dodając kolejny plik „open” z literą „w” do powyższego kodu przed blokadą. Jest to jednak nieuniknione, ponieważ musisz otworzyć plik przed zablokowaniem go. Aby wyjaśnić, termin „atomowy” oznacza, że ​​w pliku zostanie znaleziona tylko legalna zawartość pliku. Oznacza to, że nigdy nie otrzymasz pliku z zawartością wielu konkurujących ze sobą procesów.
Thomas Lux
15

Wolę lockfile - niezależne od platformy blokowanie plików

ferrdo
źródło
3
Ta biblioteka wydaje się dobrze napisana, ale nie ma mechanizmu wykrywania nieaktualnych plików blokad. Śledzi PID, który utworzył blokadę, więc powinno być możliwe stwierdzenie, czy ten proces nadal działa.
Sherbang
1
@sherbang: a co z remove_existing_pidfile ?
Janus Troelsen
@JanusTroelsen moduł pidlockfile nie uzyskuje blokad atomowo.
sherbang
@sherbang Czy na pewno? Otwiera plik blokady w trybie O_CREAT | O_EXCL.
mhsmith
2
Należy pamiętać, że ta biblioteka została zastąpiona i jest częścią github.com/harlowja/fasteners
congusbongus
13

Szukałem kilku rozwiązań, aby to zrobić, i mój wybór to oslo.concurrency

Jest potężny i stosunkowo dobrze udokumentowany. Opiera się na elementach złącznych.

Inne rozwiązania:

Maxime Viargues
źródło
re: Portalocker, możesz teraz zainstalować pywin32 przez pip za pośrednictwem pakietu pypiwin32.
Timothy Jannace
13

Blokowanie zależy od platformy i urządzenia, ale ogólnie masz kilka opcji:

  1. Użyj flock () lub odpowiednika (jeśli Twój system operacyjny to obsługuje). Jest to blokada zalecana, chyba że sprawdzisz zamek, jest on ignorowany.
  2. Skorzystaj z metodologii lock-copy-move-unlock, w której kopiujesz plik, zapisujesz nowe dane, a następnie przenosisz je (przenoszenie, nie kopiowanie - przenoszenie jest niepodzielną operacją w systemie Linux - sprawdź system operacyjny) i sprawdzasz, czy istnienie pliku blokady.
  3. Użyj katalogu jako „blokady”. Jest to konieczne, jeśli piszesz do NFS, ponieważ NFS nie obsługuje flock ().
  4. Istnieje również możliwość korzystania z pamięci współdzielonej między procesami, ale nigdy tego nie próbowałem; jest bardzo specyficzny dla systemu operacyjnego.

W przypadku wszystkich tych metod będziesz musiał użyć techniki blokowania spinu (ponowna próba po awarii) w celu uzyskania i przetestowania blokady. To pozostawia małe okno na błędną synchronizację, ale generalnie jest na tyle małe, że nie stanowi poważnego problemu.

Jeśli szukasz rozwiązania wieloplatformowego, lepiej zaloguj się do innego systemu za pomocą innego mechanizmu (następną najlepszą rzeczą jest powyższa technika NFS).

Zauważ, że sqlite podlega tym samym ograniczeniom w systemie plików NFS, co normalne pliki, więc nie możesz pisać do bazy danych sqlite w udziale sieciowym i uzyskiwać synchronizacji za darmo.

Richard Levasseur
źródło
4
Uwaga: funkcja Move / Rename nie jest atomowa w systemie Win32.
Źródła
4
Nowa uwaga: os.renamejest teraz atomowa w Win32 od Pythona 3.3: bugs.python.org/issue8828
Ghostkeeper
7

Koordynacja dostępu do pojedynczego pliku na poziomie systemu operacyjnego jest pełna różnego rodzaju problemów, których prawdopodobnie nie chcesz rozwiązać.

Najlepiej jest mieć oddzielny proces, który koordynuje dostęp do odczytu / zapisu do tego pliku.

Kevin
źródło
19
„oddzielny proces, który koordynuje dostęp do odczytu / zapisu do tego pliku” - innymi słowy, zaimplementuj serwer bazy danych :-)
Eli Bendersky,
1
To właściwie najlepsza odpowiedź. Samo powiedzenie „użyj serwera bazy danych” jest zbyt uproszczone, ponieważ baza danych nie zawsze będzie odpowiednim narzędziem do tego zadania. A co, jeśli musi to być zwykły plik tekstowy? Dobrym rozwiązaniem może być utworzenie procesu potomnego, a następnie dostęp do niego przez nazwany potok, gniazdo unixowe lub pamięć współdzieloną.
Brendon Crawford,
9
-1, ponieważ to tylko FUD bez wyjaśnienia. Zablokowanie pliku do zapisu wydaje mi się dość prostą koncepcją, ponieważ systemy operacyjne oferują podobne funkcje flock. Podejście „rzuć własne muteksy i proces demona, aby nimi zarządzać” wydaje się być raczej ekstremalnym i skomplikowanym podejściem do rozwiązania ... problem, o którym tak naprawdę nam nie powiedziałeś, ale po prostu sugerowałeś, że istnieje.
Mark Amery,
-1 z powodów podanych przez @Mark Amery, a także za przedstawienie bezpodstawnej opinii na temat problemów, które OP chce rozwiązać
Michael Krebs
5

Blokowanie pliku jest zwykle operacją specyficzną dla platformy, więc może być konieczne zezwolenie na uruchomienie w różnych systemach operacyjnych. Na przykład:

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"
Greg Hewgill
źródło
7
Być może już to wiesz, ale moduł platformy jest również dostępny, aby uzyskać informacje o uruchomionej platformie. platform.system (). docs.python.org/library/platform.html .
monkut
2

Pracowałem nad taką sytuacją, w której uruchamiam wiele kopii tego samego programu z tego samego katalogu / folderu i rejestruję błędy. Moje podejście polegało na zapisaniu „pliku blokady” na dysku przed otwarciem pliku dziennika. Program sprawdza obecność „pliku blokady” przed kontynuowaniem i czeka na swoją kolej, jeśli „plik blokady” istnieje.

Oto kod:

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')

EDYTUJ --- Po przemyśleniu niektórych komentarzy dotyczących nieaktualnych blokad powyżej, zredagowałem kod, aby dodać sprawdzenie nieaktualności „pliku blokady”. Czas kilku tysięcy iteracji tej funkcji w moim systemie dał średnio 0,002066 ... sekund tuż przed:

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

zaraz po:

remove('errloglock')

więc doszedłem do wniosku, że zacznę od 5 razy większej ilości, aby wskazać nieaktualność i monitorować sytuację pod kątem problemów.

Ponadto, pracując z synchronizacją, zdałem sobie sprawę, że mam kawałek kodu, który nie był naprawdę potrzebny:

lock.close()

które miałem natychmiast po otwartym oświadczeniu, więc usunąłem go w tej edycji.

Biała broda
źródło
2

Aby dodać do odpowiedzi Evana Fossmarka , oto przykład użycia blokady 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()

Każdy kod w with lock:bloku jest bezpieczny dla wątków, co oznacza, że ​​zostanie zakończony, zanim inny proces uzyska dostęp do pliku.

Josh Correia
źródło
1

Scenariusz jest tak: użytkownik zażąda plik do zrobienia czegoś. Następnie, jeśli użytkownik ponownie wyśle ​​to samo żądanie, informuje użytkownika, że ​​drugie żądanie nie zostanie wykonane, dopóki pierwsze żądanie nie zakończy się. Dlatego używam mechanizmu blokującego, aby rozwiązać ten problem.

Oto mój działający kod:

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
źródło
0

Znalazłem prostą i działającą (!) Implementację z grizzled-python.

Proste użycie os.open (..., O_EXCL) + os.close () nie działało w systemie Windows.

Speq
źródło
4
Opcja O_EXCL nie jest związana z zamkiem
Siergiej
0

Pylocker może być bardzo przydatny. Może być używany do blokowania pliku lub ogólnie do blokowania mechanizmów i może być dostępny z wielu procesów Pythona jednocześnie.

Jeśli chcesz po prostu zablokować plik, oto jak to działa:

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
źródło