Blocco di un file in Python


152

Devo bloccare un file per scrivere in Python. Vi si accederà da più processi Python contemporaneamente. Ho trovato alcune soluzioni online, ma la maggior parte fallisce per i miei scopi in quanto sono spesso solo basati su Unix o basati su Windows.

Risposte:


115

Bene, quindi ho finito con il codice che ho scritto qui, sul mio sito web il link è morto, visualizza su archive.org ( disponibile anche su GitHub ). Posso usarlo nel modo seguente:

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
Come notato da un commento sul post del blog, questa soluzione non è "perfetta", in quanto è possibile che il programma venga chiuso in modo tale che il blocco venga lasciato in posizione e sia necessario eliminare manualmente il blocco prima del file diventa di nuovo accessibile. Tuttavia, a parte questo, questa è ancora una buona soluzione.
leetNightshade

3
Un'altra versione migliorata di FileLock di Evan è disponibile qui: github.com/ilastik/lazyflow/blob/master/lazyflow/utility/…
Stuart Berg

3
OpenStack ha pubblicato la propria implementazione (beh, Skip Montanaro) - pylockfile - Molto simile a quelli menzionati nei commenti precedenti, ma vale comunque la pena dare un'occhiata.
jweyrich,

7
@jweyrich Openstacks pylockfile è ora obsoleto. Si consiglia invece di utilizzare elementi di fissaggio o oslo.concurrency .
Harbun,

2
Un'altra implementazione simile suppongo: github.com/benediktschmitt/py-filelock
herry

39

Esiste un modulo di blocco dei file multipiattaforma qui: Portalocker

Anche se come dice Kevin, scrivere su un file da più processi contemporaneamente è qualcosa che vuoi evitare se possibile.

Se puoi calzare il tuo problema in un database, puoi usare SQLite. Supporta l'accesso simultaneo e gestisce il proprio blocco.


16
+1: SQLite è quasi sempre la strada da percorrere in questo tipo di situazioni.
cdleary,

2
Portalocker richiede Python Extensions per Windows, su questo.
n611x007,

2
@naxa ce n'è una variante che si basa solo su msvcrt e ctypes, vedi roundup.hg.sourceforge.net/hgweb/roundup/roundup/file/tip/…
Shmil The Cat

@ n611x007 Portalocker è appena stato aggiornato, quindi non richiede più estensioni su Windows :)
Wolph

2
SQLite supporta l'accesso simultaneo?
piotr,

23

Le altre soluzioni citano molte basi di codice esterne. Se preferisci farlo da solo, ecco un po 'di codice per una soluzione multipiattaforma che utilizza i rispettivi strumenti di blocco dei file su sistemi 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        

Ora, AtomicOpenpuò essere utilizzato in un withblocco in cui si userebbe normalmente openun'istruzione.

ATTENZIONE: se l'esecuzione su Windows e Python si arresta in modo anomalo prima che venga chiamato exit , non sono sicuro di quale sarebbe il comportamento del blocco.

ATTENZIONE: il blocco fornito qui è consultivo, non assoluto. Tutti i processi potenzialmente concorrenti devono utilizzare la classe "AtomicOpen".


unlock_filefile su linux non dovrebbe richiamare di fcntlnuovo con il LOCK_UNflag?
Maestro

Lo sblocco avviene automaticamente alla chiusura dell'oggetto file. Tuttavia, è stata una cattiva pratica di programmazione da parte mia non includerla. Ho aggiornato il codice e aggiunto l'operazione di sblocco fcntl!
Thomas Lux,

In __exit__voi closeal di fuori della serratura dopo unlock_file. Credo che il runtime potrebbe scaricare (cioè scrivere) i dati durante close. Credo che si debba flushe fsyncsotto il blocco per assicurarsi che nessun dato aggiuntivo venga scritto all'esterno del blocco durante close.
Benjamin Bannier

Grazie per la correzione! Ho verificato che non v'è la possibilità di una condizione di competizione senza la flushe fsync. Ho aggiunto le due linee che hai suggerito prima di chiamare unlock. Ho testato nuovamente e le condizioni di gara sembrano essere state risolte.
Thomas Lux,

1
L'unica cosa che andrà "male" è che quando il processo 1 bloccherà il file il suo contenuto verrà troncato (contenuto cancellato). Puoi testarlo tu stesso aggiungendo un altro file "apri" con una "w" al codice sopra prima del blocco. Ciò è inevitabile, tuttavia, perché è necessario aprire il file prima di bloccarlo. Per chiarire, "atomico" è nel senso che solo i contenuti legittimi del file saranno trovati in un file. Ciò significa che non otterrai mai un file con i contenuti di più processi concorrenti mescolati insieme.
Thomas Lux,

15

Preferisco lockfile - Blocco dei file indipendente dalla piattaforma


3
Questa libreria sembra ben scritta, ma non esiste alcun meccanismo per rilevare file di blocco non aggiornati. Tiene traccia del PID che ha creato il blocco, quindi dovrebbe essere possibile dire se quel processo è ancora in esecuzione.
Sherbang,

1
@sherbang: che dire di remove_existing_pidfile ?
Janus Troelsen,

@JanusTroelsen il modulo pidlockfile non acquisisce i blocchi atomicamente.
Sherbang,

@sherbang Sei sicuro? Apre il file di blocco con la modalità O_CREAT | O_EXCL.
mhsmith,

2
Questa libreria è stata sostituita e fa parte di github.com/harlowja/fasteners
congusbongus,

13

Ho cercato diverse soluzioni per farlo e la mia scelta è stata oslo.concurrency

È potente e relativamente ben documentato. Si basa su elementi di fissaggio.

Altre soluzioni:


re: Portalocker, ora è possibile installare pywin32 tramite pip tramite il pacchetto pypiwin32.
Timothy Jannace,


13

Il blocco è specifico per piattaforma e dispositivo, ma in genere sono disponibili alcune opzioni:

  1. Usa flock () o equivalente (se il tuo sistema operativo lo supporta). Questo è un blocco di avviso, a meno che non si controlli il blocco, viene ignorato.
  2. Usa una metodologia di blocco-copia-sposta-sblocca, in cui copi il file, scrivi i nuovi dati, quindi spostali (sposta, non copia - spostare è un'operazione atomica in Linux - controlla il tuo sistema operativo) e verifica la presenza di esistenza del file di blocco.
  3. Utilizzare una directory come "blocco". Questo è necessario se stai scrivendo su NFS, poiché NFS non supporta flock ().
  4. C'è anche la possibilità di utilizzare la memoria condivisa tra i processi, ma non l'ho mai provato; è molto specifico del sistema operativo.

Per tutti questi metodi, dovrai utilizzare una tecnica spin-lock (retry-after-failure) per acquisire e testare il blocco. Questo lascia una piccola finestra per una errata sincronizzazione, ma è generalmente abbastanza piccolo da non essere un grosso problema.

Se stai cercando una soluzione che sia multipiattaforma, allora è meglio accedere a un altro sistema tramite qualche altro meccanismo (la prossima cosa migliore è la tecnica NFS sopra).

Si noti che sqlite è soggetto agli stessi vincoli su NFS dei file normali, quindi non è possibile scrivere su un database sqlite su una condivisione di rete e ottenere la sincronizzazione gratuitamente.


4
Nota: Sposta / Rinomina non è atomico in Win32. Riferimento: stackoverflow.com/questions/167414/...
sherbang

4
Nuova nota: os.renameora è atomico in Win32 da Python 3.3: bugs.python.org/issue8828
Ghostkeeper

7

Il coordinamento dell'accesso a un singolo file a livello di sistema operativo è irto di tutti i tipi di problemi che probabilmente non si desidera risolvere.

La tua scommessa migliore è avere un processo separato che coordina l'accesso in lettura / scrittura a quel file.


19
"processo separato che coordina l'accesso in lettura / scrittura a quel file" - in altre parole, implementa un server di database :-)
Eli Bendersky

1
Questa è in realtà la migliore risposta. Dire semplicemente "usa un server di database" è troppo semplificato, poiché un db non sarà sempre lo strumento giusto per il lavoro. Cosa succede se deve essere un file di testo semplice? Una buona soluzione potrebbe essere quella di generare un processo figlio e quindi accedervi tramite una pipe denominata, un socket unix o una memoria condivisa.
Brendon Crawford,

9
-1 perché questo è solo FUD senza spiegazione. Il blocco di un file per la scrittura mi sembra un concetto piuttosto semplice che i sistemi operativi offrono con funzioni simili flock. Un approccio di "tira i tuoi mutex e un processo daemon per gestirli" sembra un approccio piuttosto estremo e complicato da adottare per risolvere ... un problema di cui non ci hai effettivamente parlato, ma che è appena stato suggerito.
Mark Amery,

-1 per le ragioni fornite da @Mark Amery, nonché per offrire un'opinione non comprovata su quali problemi l'OP vuole risolvere
Michael Krebs

5

Il blocco di un file è in genere un'operazione specifica della piattaforma, pertanto potrebbe essere necessario consentire la possibilità di essere eseguito su diversi sistemi operativi. Per esempio:

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
Potresti già saperlo, ma il modulo della piattaforma è disponibile anche per ottenere informazioni sulla piattaforma in esecuzione. platform.system (). docs.python.org/library/platform.html .
monkut,

2

Ho lavorato su una situazione come questa in cui eseguo più copie dello stesso programma all'interno della stessa directory / cartella ed errori di registrazione. Il mio approccio era quello di scrivere un "file di blocco" sul disco prima di aprire il file di registro. Il programma verifica la presenza del "file di blocco" prima di procedere e attende il suo turno se esiste il "file di blocco".

Ecco il codice:

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

MODIFICA --- Dopo aver riflettuto su alcuni dei commenti sui blocchi obsoleti sopra ho modificato il codice per aggiungere un controllo per la staleness del "file di blocco". Il tempismo di diverse migliaia di iterazioni di questa funzione sul mio sistema ha dato e una media di 0,002066 ... secondi da poco prima:

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

subito dopo:

remove('errloglock')

così ho pensato che inizierò con 5 volte tale importo per indicare la stanchezza e monitorare la situazione per problemi.

Inoltre, mentre stavo lavorando con i tempi, mi sono reso conto che avevo un po 'di codice che non era davvero necessario:

lock.close()

che ho subito seguito alla dichiarazione aperta, quindi l'ho rimosso in questa modifica.


2

Per aggiungere alla risposta di Evan Fossmark , ecco un esempio di come utilizzare 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()

Qualsiasi codice all'interno del with lock:blocco è thread-safe, il che significa che sarà completato prima che un altro processo abbia accesso al file.


1

Lo scenario è simile: l'utente richiede un file per fare qualcosa. Quindi, se l'utente invia nuovamente la stessa richiesta, informa l'utente che la seconda richiesta non viene eseguita fino al termine della prima richiesta. Ecco perché, uso il meccanismo di blocco per gestire questo problema.

Ecco il mio codice di lavoro:

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

Ho trovato un'implementazione semplice e funzionante (!) Da Grizzled-Python.

Uso semplice os.open (..., O_EXCL) + os.close () non ha funzionato su Windows.


4
L'opzione O_EXCL non è correlata al blocco
Sergei

0

Potresti trovare molto utile pylocker . Può essere usato per bloccare un file o per meccanismi di blocco in generale e vi si può accedere da più processi Python contemporaneamente.

Se vuoi semplicemente bloccare un file, ecco come funziona:

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.