Bloqueando um arquivo em Python

152

Preciso bloquear um arquivo para escrever em Python. Ele será acessado de vários processos Python de uma só vez. Encontrei algumas soluções on-line, mas a maioria falha para meus propósitos, pois geralmente são baseadas apenas em Unix ou Windows.

Evan Fosmark
fonte

Respostas:

115

Tudo bem, então eu acabei seguindo o código que escrevi aqui, no link do meu site está morto, veja em archive.org ( também disponível no GitHub ). Eu posso usá-lo da seguinte maneira:

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
fonte
10
Conforme observado por um comentário na postagem do blog, essa solução não é "perfeita", pois é possível que o programa seja finalizado de forma que o bloqueio seja deixado no lugar e você precise excluir manualmente o bloqueio antes do arquivo torna-se acessível novamente. No entanto, isso de lado, ainda é uma boa solução.
precisa saber é o seguinte
3
Ainda outra versão aprimorada do FileLock do Evan pode ser encontrada aqui: github.com/ilastik/lazyflow/blob/master/lazyflow/utility/…
Stuart Berg
3
O OpenStack publicou sua própria implementação (bem, de Skip Montanaro) - pylockfile - muito semelhante à mencionada nos comentários anteriores, mas ainda vale a pena dar uma olhada.
jweyrich
7
@jweyrich Openstacks O pylockfile agora está obsoleto. É recomendável usar fixadores ou oslo.concurrency .
harbun
2
Outra implementação semelhante, eu acho: github.com/benediktschmitt/py-filelock
herry
39

Há um módulo de bloqueio de arquivos de plataforma cruzada aqui: Portalocker

Embora, como Kevin diz, gravar em um arquivo de vários processos ao mesmo tempo seja algo que você deseja evitar, se possível.

Se você pode colocar o seu problema em um banco de dados, pode usar o SQLite. Ele suporta acesso simultâneo e lida com seu próprio bloqueio.

John Fouhy
fonte
16
+1 - SQLite é quase sempre o caminho a seguir nesse tipo de situação.
cdleary
2
O Portalocker requer extensões do Python para Windows.
N611x007
2
@naxa há uma variante dela que se baseia apenas em msvcrt e ctypes, ver roundup.hg.sourceforge.net/hgweb/roundup/roundup/file/tip/...
Shmil The Cat
@ n611x007 Portalocker acaba de ser atualizado para que ele não necessita de quaisquer extensões no Windows mais :)
Wolph
2
SQLite suporta acesso simultâneo?
Piotr #
23

As outras soluções citam muitas bases de código externas. Se você preferir fazê-lo, aqui está um código para uma solução de plataforma cruzada que usa as respectivas ferramentas de bloqueio de arquivos nos 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        

Agora, AtomicOpenpode ser usado em um withbloco em que normalmente se usaria uma openinstrução.

AVISO: Se a execução no Windows e no Python falharem antes da saída ser chamada, não tenho certeza de qual seria o comportamento do bloqueio.

AVISO: O bloqueio fornecido aqui é um aviso, não absoluto. Todos os processos potencialmente concorrentes devem usar a classe "AtomicOpen".

Thomas Lux
fonte
unlock_filearquivo no linux não deve ligar fcntlnovamente com a LOCK_UNbandeira?
Eadmaster #
O desbloqueio acontece automaticamente quando o objeto de arquivo é fechado. No entanto, foi uma má prática de programação da minha parte não incluí-lo. Atualizei o código e adicionei a operação de desbloqueio do fcntl!
Thomas Lux
Em __exit__você closefora da fechadura depois unlock_file. Acredito que o tempo de execução possa liberar (ou seja, gravar) dados durante close. Eu acredito que é preciso flushe fsyncsob o bloqueio para garantir que nenhum dado adicional seja gravado fora do bloqueio durante close.
Benjamin Bannier
Obrigado pela correção! I verificado que não é a possibilidade de uma condição de corrida sem a flushe fsync. Adicionei as duas linhas que você sugeriu antes de ligar unlock. Testei novamente e a condição de corrida parece estar resolvida.
Thomas Lux
1
A única coisa que vai "errar" é que, quando o processo 1 bloquear o arquivo, seu conteúdo será truncado (conteúdo apagado). Você pode testar isso adicionando outro arquivo "aberto" com um "w" ao código acima antes do bloqueio. Isso é inevitável, porque você deve abrir o arquivo antes de bloqueá-lo. Para esclarecer, o "atômico" é no sentido de que apenas o conteúdo legítimo do arquivo será encontrado em um arquivo. Isso significa que você nunca obterá um arquivo com conteúdo de vários processos concorrentes misturados.
Thomas Lux
15

Eu prefiro lockfile - bloqueio de arquivo independente de plataforma

ferrdo
fonte
3
Essa biblioteca parece bem escrita, mas não há mecanismo para detectar arquivos de bloqueio antigos. Ele rastreia o PID que criou o bloqueio, portanto, é possível saber se esse processo ainda está em execução.
sherbang
1
@ Sherbang: o que dizer de remove_existing_pidfile ?
Janus Troelsen
@JanusTroelsen o módulo pidlockfile não adquire bloqueios atomicamente.
sherbang
@sherbang Você tem certeza? Abre o arquivo de bloqueio com o modo O_CREAT | O_EXCL.
mhsmith
2
Observe que esta biblioteca foi substituída e faz parte de github.com/harlowja/fasteners
congusbongus
13

Estive procurando várias soluções para fazer isso e minha escolha foi oslo.concurrency

É poderoso e relativamente bem documentado. É baseado em fixadores.

Outras soluções:

Maxime Viargues
fonte
re: Portalocker, agora você pode instalar o pywin32 através do pip através do pacote pypiwin32.
Timothy Jannace 18/09/18
13

O bloqueio é específico da plataforma e do dispositivo, mas geralmente você tem algumas opções:

  1. Use flock () ou equivalente (se o seu SO suportar). Este é um bloqueio consultivo, a menos que você verifique se o bloqueio é ignorado.
  2. Use uma metodologia de bloqueio, cópia, movimentação e desbloqueio, onde você copia o arquivo, grava os novos dados e os move (mover, não copiar - mover é uma operação atômica no Linux - verifique seu sistema operacional) e verifique o existência do arquivo de bloqueio.
  3. Use um diretório como um "bloqueio". Isso é necessário se você estiver gravando no NFS, pois o NFS não suporta flock ().
  4. Há também a possibilidade de usar memória compartilhada entre os processos, mas nunca tentei isso; é muito específico do sistema operacional.

Para todos esses métodos, você precisará usar uma técnica de bloqueio de rotação (repetir após falha) para adquirir e testar o bloqueio. Isso deixa uma pequena janela para sincronização incorreta, mas geralmente é pequena o suficiente para não ser um problema importante.

Se você está procurando uma solução que seja multiplataforma, é melhor fazer logon em outro sistema por outro mecanismo (a próxima melhor coisa é a técnica NFS acima).

Observe que o sqlite está sujeito às mesmas restrições do NFS que os arquivos normais, portanto, você não pode gravar em um banco de dados do sqlite em um compartilhamento de rede e obter sincronização gratuitamente.

Richard Levasseur
fonte
4
Nota: Mover / Renomear não é atômico no Win32. Referência: stackoverflow.com/questions/167414/…
sherbang
4
Nova nota: os.renameagora é atômica no Win32 desde o Python 3.3: bugs.python.org/issue8828
Ghostkeeper em 29/08/16
7

A coordenação do acesso a um único arquivo no nível do sistema operacional está repleta de todos os tipos de problemas que você provavelmente não deseja resolver.

Sua melhor aposta é ter um processo separado que coordene o acesso de leitura / gravação a esse arquivo.

Kevin
fonte
19
"processo separado que as coordenadas acesso de leitura / gravação para o arquivo" - em outras palavras, implementar um servidor de banco de dados :-)
Eli Bendersky
1
Esta é realmente a melhor resposta. Dizer apenas "usar um servidor de banco de dados" é muito simplificado, pois um banco de dados nem sempre será a ferramenta certa para o trabalho. E se precisar ser um arquivo de texto sem formatação? Uma boa solução pode ser gerar um processo filho e acessá-lo por meio de um pipe nomeado, soquete unix ou memória compartilhada.
Brendon Crawford
9
-1 porque este é apenas FUD sem explicação. Bloquear um arquivo para escrever parece um conceito bastante claro para mim de que os sistemas operacionais oferecem funções semelhantes flock. Uma abordagem de "role seus próprios mutexes e um processo de daemon para gerenciá-los" parece ser uma abordagem bastante extrema e complicada a ser tomada para resolver ... um problema que você não nos falou, mas que sugere assustadoramente.
Marque Amery
-1 para as razões dadas pelo @ Marcos Amery, bem como para oferecer uma opinião sem fundamento sobre o qual questões do OP quer resolver
Michael Krebs
5

O bloqueio de um arquivo geralmente é uma operação específica da plataforma; portanto, talvez seja necessário permitir a execução em diferentes sistemas operacionais. Por exemplo:

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
fonte
7
Você já deve saber disso, mas o módulo da plataforma também está disponível para obter informações sobre a plataforma em execução. platform.system (). docs.python.org/library/platform.html .
monkut
2

Eu tenho trabalhado em uma situação como essa em que executo várias cópias do mesmo programa de dentro do mesmo diretório / pasta e erros de log. Minha abordagem foi gravar um "arquivo de bloqueio" no disco antes de abrir o arquivo de log. O programa verifica a presença do "arquivo de bloqueio" antes de continuar e aguarda sua vez se o "arquivo de bloqueio" existir.

Aqui está o 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 --- Depois de refletir sobre alguns dos comentários sobre bloqueios obsoletos acima, editei o código para adicionar uma verificação de robustez do "arquivo de bloqueio". O tempo de vários milhares de iterações dessa função no meu sistema deu uma média de 0,002066 ... segundos antes:

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

logo depois:

remove('errloglock')

então imaginei que começaria com 5 vezes esse valor para indicar a rigidez e monitorar a situação quanto a problemas.

Além disso, ao trabalhar com o tempo, percebi que tinha um pouco de código que não era realmente necessário:

lock.close()

que eu tinha imediatamente após a declaração aberta, por isso a removi nesta edição.

barba branca
fonte
2

Para adicionar à resposta de Evan Fossmark , aqui está um exemplo de como usar o 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()

Qualquer código dentro do with lock:bloco é seguro para threads, o que significa que será concluído antes que outro processo tenha acesso ao arquivo.

Josh Correia
fonte
1

O cenário é o seguinte: O usuário solicita um arquivo para fazer alguma coisa. Em seguida, se o usuário enviar a mesma solicitação novamente, ele informará que a segunda solicitação não será feita até que a primeira solicitação seja concluída. É por isso que eu uso o mecanismo de bloqueio para lidar com esse problema.

Aqui está o meu código de trabalho:

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

Eu encontrei uma implementação simples e trabalhada (!) Do grizzled-python.

O uso simples os.open (..., O_EXCL) + os.close () não funcionou no Windows.

Speq
fonte
4
A opção O_EXCL não está relacionada ao bloqueio
Sergei
0

Você pode achar o pylocker muito útil. Ele pode ser usado para bloquear um arquivo ou para mecanismos de bloqueio em geral e pode ser acessado a partir de vários processos Python ao mesmo tempo.

Se você simplesmente deseja bloquear um arquivo, veja como ele 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
fonte