Bloquear un archivo en Python

152

Necesito bloquear un archivo para escribir en Python. Se accederá desde múltiples procesos de Python a la vez. He encontrado algunas soluciones en línea, pero la mayoría fallan para mis propósitos, ya que a menudo solo están basadas en Unix o Windows.

Evan Fosmark
fuente

Respuestas:

115

Muy bien, así que terminé yendo con el código que escribí aquí, en el enlace de mi sitio web está muerto, ver en archive.org ( también disponible en GitHub ). Puedo usarlo de la siguiente manera:

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
fuente
10
Como lo señaló un comentario en la publicación del blog, esta solución no es "perfecta", ya que es posible que el programa finalice de tal manera que el bloqueo se deje en su lugar y tenga que eliminarlo manualmente antes del archivo se vuelve accesible de nuevo. Sin embargo, aparte de eso, esta sigue siendo una buena solución.
leetNightshade
3
Otra versión mejorada del FileLock de Evan se puede encontrar aquí: github.com/ilastik/lazyflow/blob/master/lazyflow/utility/…
Stuart Berg
3
OpenStack publicó su propia implementación (bueno, Skip Montanaro), pylockfile , muy similar a las mencionadas en comentarios anteriores, pero aún así vale la pena echarle un vistazo.
jweyrich
77
@jweyrich Openstacks pylockfile ahora está en desuso. Se recomienda usar sujetadores u oslo.concurrency en su lugar.
Harbun
2
Otra implementación similar, supongo: github.com/benediktschmitt/py-filelock
herry
39

Aquí hay un módulo de bloqueo de archivos multiplataforma: Portalocker

Aunque, como dice Kevin, escribir en un archivo desde múltiples procesos a la vez es algo que desea evitar si es posible.

Si puede calzar su problema en una base de datos, puede usar SQLite. Admite acceso concurrente y maneja su propio bloqueo.

John Fouhy
fuente
16
+1: SQLite es casi siempre el camino a seguir en este tipo de situaciones.
cdleary
2
Portalocker requiere Python Extensions para Windows, en eso.
n611x007
2
@naxa hay una variante que se basa solo en msvcrt y ctypes, ver roundup.hg.sourceforge.net/hgweb/roundup/roundup/file/tip/…
Shmil The Cat
@ n611x007 Portalocker acaba de actualizarse, por lo que ya no requiere ninguna extensión en Windows :)
Wolph
2
SQLite admite acceso concurrente?
piotr
23

Las otras soluciones citan muchas bases de código externas. Si prefiere hacerlo usted mismo, aquí hay un código para una solución multiplataforma que utiliza las respectivas herramientas de bloqueo de archivos en sistemas 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        

Ahora, AtomicOpense puede usar en un withbloque donde normalmente se usaría una opendeclaración.

ADVERTENCIA: Si se ejecuta en Windows y Python se bloquea antes de que se llame a exit , no estoy seguro de cuál sería el comportamiento del bloqueo.

ADVERTENCIA: El bloqueo proporcionado aquí es informativo, no absoluto. Todos los procesos potencialmente competitivos deben usar la clase "AtomicOpen".

Thomas Lux
fuente
unlock_filearchivo en Linux no debería fcntlvolver a llamar con la LOCK_UNbandera?
eadmaster
El desbloqueo ocurre automáticamente cuando se cierra el objeto del archivo. Sin embargo, fue una mala práctica de programación por mi parte no incluirlo. ¡He actualizado el código y agregué la operación de desbloqueo fcntl!
Thomas Lux
En __exit__ti closefuera de la cerradura después unlock_file. Creo que el tiempo de ejecución podría vaciar (es decir, escribir) datos durante close. Creo que uno debe flushy fsyncdebajo de la cerradura para asegurarse de que no se escriban datos adicionales fuera de la cerradura durante close.
Benjamin Bannier
¡Gracias por la corrección! He verificado que existe es la posibilidad de que una condición de carrera sin la flushy fsync. He agregado las dos líneas que sugirió antes de llamar unlock. Volví a probar y la condición de carrera parece estar resuelta.
Thomas Lux
1
Lo único que saldrá "mal" es que cuando el proceso 1 bloquea el archivo, su contenido se truncará (se borrará). Puede probar esto usted mismo agregando otro archivo "abierto" con una "w" al código anterior antes del bloqueo. Sin embargo, esto es inevitable porque debe abrir el archivo antes de bloquearlo. Para aclarar, lo "atómico" es en el sentido de que solo se encontrarán contenidos legítimos en un archivo. Esto significa que nunca obtendrá un archivo con contenido de múltiples procesos competidores mezclados.
Thomas Lux el
15

Prefiero lockfile : bloqueo de archivos independiente de la plataforma

ferrdo
fuente
3
Esta biblioteca parece estar bien escrita, pero no existe un mecanismo para detectar archivos de bloqueo obsoletos. Rastrea el PID que creó el bloqueo, por lo que debería ser posible saber si ese proceso aún se está ejecutando.
sherbang
1
@sherbang: ¿qué pasa con remove_existing_pidfile ?
Janus Troelsen
@JanusTroelsen el módulo pidlockfile no adquiere bloqueos atómicamente.
Sherbang
@sherbang ¿Estás seguro? Abre el archivo de bloqueo con el modo O_CREAT | O_EXCL.
mhsmith
2
Tenga en cuenta que esta biblioteca ha sido reemplazada y es parte de github.com/harlowja/fasteners
congusbongus
13

He estado buscando varias soluciones para hacer eso y mi elección ha sido oslo.concurrency

Es potente y relativamente bien documentado. Se basa en sujetadores.

Otras soluciones:

Maxime Viargues
fuente
re: Portalocker, ahora puede instalar pywin32 a través de pip a través del paquete pypiwin32.
Timothy Jannace
13

El bloqueo es específico de la plataforma y el dispositivo, pero en general, tiene algunas opciones:

  1. Use flock (), o equivalente (si su sistema operativo lo admite). Este es un bloqueo de aviso, a menos que verifique el bloqueo, se ignora.
  2. Use una metodología de bloqueo-copia-movimiento-desbloqueo, donde copie el archivo, escriba los nuevos datos, luego muévalo (mover, no copiar - mover es una operación atómica en Linux - verifique su sistema operativo), y verifique el existencia del archivo de bloqueo.
  3. Utilice un directorio como "bloqueo". Esto es necesario si está escribiendo en NFS, ya que NFS no admite flock ().
  4. También existe la posibilidad de usar memoria compartida entre los procesos, pero nunca lo he intentado; Es muy específico del sistema operativo.

Para todos estos métodos, deberá utilizar una técnica de bloqueo de giro (reintentar después de fallar) para adquirir y probar el bloqueo. Esto deja una pequeña ventana para la sincronización incorrecta, pero generalmente es lo suficientemente pequeña como para no ser un problema importante.

Si está buscando una solución que sea multiplataforma, es mejor que inicie sesión en otro sistema a través de algún otro mecanismo (la siguiente mejor opción es la técnica NFS anterior).

Tenga en cuenta que sqlite está sujeto a las mismas restricciones sobre NFS que los archivos normales, por lo que no puede escribir en una base de datos sqlite en un recurso compartido de red y obtener la sincronización de forma gratuita.

Richard Levasseur
fuente
44
Nota: Mover / Cambiar nombre no es atómico en Win32. Referencia: stackoverflow.com/questions/167414/…
sherbang
44
Nueva nota: os.renameahora es atómica en Win32 desde Python 3.3: bugs.python.org/issue8828
Ghostkeeper
7

Coordinar el acceso a un solo archivo en el nivel del sistema operativo está lleno de todo tipo de problemas que probablemente no desee resolver.

Su mejor opción es tener un proceso separado que coordine el acceso de lectura / escritura a ese archivo.

Kevin
fuente
19
"proceso separado que coordina el acceso de lectura / escritura a ese archivo" - en otras palabras, implemente un servidor de base de datos :-)
Eli Bendersky
1
Esta es realmente la mejor respuesta. Simplemente decir "usar un servidor de base de datos" se simplifica demasiado, ya que un db no siempre será la herramienta adecuada para el trabajo. ¿Qué pasa si necesita ser un archivo de texto sin formato? Una buena solución podría ser generar un proceso secundario y luego acceder a él a través de una tubería con nombre, un socket o memoria compartida.
Brendon Crawford
9
-1 porque esto es solo FUD sin explicación. Bloquear un archivo para escribir me parece un concepto bastante sencillo para mí que los sistemas operativos ofrecen funciones similares flock. Un enfoque de "rodar sus propios mutexes y un proceso de demonio para administrarlos" parece un enfoque bastante extremo y complicado de resolver ... un problema que no nos ha contado realmente, pero que apenas sugiere que existe.
Mark Amery
-1 por las razones dadas por @Mark Amery, así como por ofrecer una opinión sin fundamento sobre los problemas que el OP quiere resolver
Michael Krebs,
5

El bloqueo de un archivo suele ser una operación específica de la plataforma, por lo que es posible que deba permitir la posibilidad de ejecutarse en diferentes sistemas operativos. Por ejemplo:

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
fuente
77
Es posible que ya lo sepas, pero el módulo de plataforma también está disponible para obtener información sobre la plataforma en ejecución. plataforma.sistema (). docs.python.org/library/platform.html .
monkut
2

He estado trabajando en una situación como esta donde ejecuto varias copias del mismo programa desde el mismo directorio / carpeta y errores de registro. Mi enfoque era escribir un "archivo de bloqueo" en el disco antes de abrir el archivo de registro. El programa comprueba la presencia del "archivo de bloqueo" antes de continuar y espera su turno si existe el "archivo de bloqueo".

Aquí está el código:

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

EDITAR --- Después de pensar en algunos de los comentarios sobre los bloqueos obsoletos anteriores, edité el código para agregar una verificación de la obsolescencia del "archivo de bloqueo". El tiempo de varios miles de iteraciones de esta función en mi sistema dio un promedio de 0.002066 ... segundos antes de:

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

justo después:

remove('errloglock')

así que pensé que comenzaría con 5 veces esa cantidad para indicar la obsolescencia y monitorear la situación en busca de problemas.

Además, mientras trabajaba con el tiempo, me di cuenta de que tenía un código que no era realmente necesario:

lock.close()

que tenía inmediatamente después de la declaración abierta, por lo que la eliminé en esta edición.

barba blanca
fuente
2

Para agregar a la respuesta de Evan Fossmark , aquí hay un ejemplo de cómo usar 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()

Cualquier código dentro del with lock:bloque es seguro para subprocesos, lo que significa que se terminará antes de que otro proceso tenga acceso al archivo.

Josh Correia
fuente
1

El escenario es así: el usuario solicita un archivo para hacer algo. Luego, si el usuario envía la misma solicitud nuevamente, informa al usuario que la segunda solicitud no se realiza hasta que finalice la primera solicitud. Por eso, uso el mecanismo de bloqueo para manejar este problema.

Aquí está mi código de trabajo:

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
fuente
0

Encontré una implementación simple y trabajada (!) De grizzled-python.

El uso simple os.open (..., O_EXCL) + os.close () no funcionó en Windows.

Speq
fuente
44
La opción O_EXCL no está relacionada con el bloqueo
Sergei
0

Puede encontrar pylocker muy útil. Se puede usar para bloquear un archivo o para bloquear mecanismos en general y se puede acceder desde múltiples procesos de Python a la vez.

Si simplemente desea bloquear un archivo, así es como funciona:

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
fuente