在Python中锁定文件

212

我需要在Python中为写入锁定一个文件。它将被多个Python进程同时访问。我在网上找到了一些解决方案,但大多数都无法满足我的需求,因为它们通常只适用于Unix或Windows操作系统。

15个回答

170

21
在博客文章的评论中提到,这种解决方案并非“完美”,因为程序可能以某种方式终止,导致锁定留在原地,您必须手动删除锁定才能再次访问该文件。然而,除此之外,这仍然是一个不错的解决方案。 - leetNightshade
3
Evan的文件锁的另一个改进版本可以在此处找到:https://github.com/ilastik/lazyflow/blob/master/lazyflow/utility/fileLock.py。 - Stuart Berg
3
OpenStack发布了他们自己(其实是Skip Montanaro的)的实现 - pylockfile - 与之前评论中提到的实现非常相似,但仍值得一看。 - jweyrich
10
OpenStack的pylockfile现已被弃用。建议使用fastenersoslo.concurrency代替。 - harbun
4
另一种类似的实现我猜:https://github.com/benediktschmitt/py-filelock - Herry
显示剩余11条评论

54

其他解决方案引用了许多外部代码库。如果你想自己完成,这里有一些跨平台解决方案的代码,它使用Linux / DOS系统上各自的文件锁定工具。

try:
    # Posix based file locking (Linux, Ubuntu, MacOS, etc.)
    #   Only allows locking on writable files, might cause
    #   strange results for reading.
    import fcntl, os
    def lock_file(f):
        if f.writable(): fcntl.lockf(f, fcntl.LOCK_EX)
    def unlock_file(f):
        if f.writable(): 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”类。
  • 截至2020年11月9日,此代码仅在Posix系统上锁定可写文件。在此发布后的某个时间点之后,使用fcntl.lock锁定只读文件已成为非法操作。

__exit__ 中,在 unlock_file 之后,你需要在锁外部进行 close。我相信运行时可能会在 close 过程中刷新(即写入)数据。我认为必须在锁下执行 flushfsync,以确保在 close 过程中不会写入额外的数据到锁外部。 - Benjamin Bannier
即使两个进程都使用它,如果第二个进程在第一个进程调用 self.file = open('somefile.txt', 'r')lock_file(self.file) 之间调用 self.file = open('somefile.txt', 'w'),事情仍然可能出错,对吗? - jez
1
唯一可能出现的问题是,当进程1锁定文件时,文件内容将被截断(内容被擦除)。您可以通过在上面的代码中添加另一个带有“w”的“打开”文件来测试此功能之前锁定。然而,这是不可避免的,因为您必须在锁定文件之前打开它。澄清一下,“原子性”是指文件中只会找到合法的文件内容。这意味着您永远不会得到来自多个竞争进程混合在一起的文件内容。 - Thomas Lux
根据我通过谷歌找到的信息,msvcrt.LK_RLCK 是读锁,因此与 fcntl.LOCK_SH 相同,而 msvcrt.LK_LOCK 是排他锁,类似于 fcntl.LOCK_EX。因此,在 Windows 下,您可能必须使用 msvcrt.LK_LOCK。然而,像往常一样,文档很糟糕,我没有 Windows 来验证,抱歉。 - Tino
1
这比在Windows上预期的要难解决。该文件由一个大小锁定,但如果内容被写入,则大小会更改(并且当前的代码在解锁时失败,因为大小不正确)。似乎Windows完全忽略了锁定。我需要进一步调查,但这需要时间。我在弄清楚后会发布解决方案。 - Thomas Lux
显示剩余5条评论

44

这里有一个跨平台的文件锁定模块: Portalocker

虽然正如 Kevin 所说,尽可能避免多个进程同时写入同一文件。

如果你可以将问题塞进数据库中,那么就可以使用 SQLite。它支持并发访问,并处理自己的锁定。


27
+1 -- 在这种情况下,几乎总是选择SQLite。 - cdleary
2
Portalocker需要Windows的Python扩展。 - n611x007
2
@naxa,有一种变体只依赖于msvcrt和ctypes,请参见http://roundup.hg.sourceforge.net/hgweb/roundup/roundup/file/tip/roundup/backends/portalocker.py。 - Shmil The Cat
2
SQLite支持并发访问吗? - piotr
1
我来到这里是因为我需要一个锁文件来修复我的SQLite数据库,它被并发损坏了。我想现在是时候烧掉我的电脑,去当一名僧侣了。 - duhaime
显示剩余3条评论

26

我一直在寻找几种解决方案,并选择了oslo.concurrency

它很强大且文档相对详细,基于fasteners。

其他解决方案:

  • Portalocker:需要pywin32,这是一个exe安装程序,无法通过pip安装
  • fasteners:文档质量较差
  • lockfile:已被弃用
  • flufl.lock:适用于POSIX系统的NFS安全文件锁定。
  • simpleflock :上次更新时间为2013-07
  • zc.lockfile :上次更新时间为2016-06(截至2017-03)
  • lock_file :上次更新时间为2007-10

1
关于Portalocker,现在你可以通过pypiwin32包通过pip安装pywin32。 - Timothy Jannace
5
还有一个名为 filelock 的库(在评论时的最新发布日期为2019年5月18日)。 - jfs
@jfs,filelock包不是与被接受的答案相同吗? 安装方法为:pip3 install filelock - alper
@alper:相同的模块名称,不同的包(请查看Github) - jfs
@jfs 建议使用哪一个?你提供的 filelock 比被采纳答案的包有更多的 Star 和 forks。 - alper
@alper 安装了我链接的模块。因此,除非您有一些更适合其他模块的敏捷要求,否则它可以被视为默认选择。 - jfs

18

我更喜欢 lockfile — 跨平台文件锁定


3
这个库似乎写得很好,但没有检测陈旧的锁文件的机制。它会追踪创建锁的进程ID,因此应该可以确定该进程是否仍在运行。 - sherbang
1
@sherbang: 对于 remove_existing_pidfile,有什么看法? - Janus Troelsen
@sherbang 你确定吗?它用O_CREAT|O_EXCL模式打开锁文件。 - mhsmith
根据文档,该软件包已被弃用。 - Damian
3
请注意,该库已经过时,现已成为https://github.com/harlowja/fasteners的一部分。 - congusbongus
显示剩余3条评论

16

锁定是平台和设备特定的,但通常有几个选项:

  1. 如果您的操作系统支持,使用flock()或等效功能。这是一个建议性锁定,除非您检查锁定否则会被忽略。
  2. 使用锁-复制-移动-解锁方法,其中您复制文件,编写新数据,然后将其移动(移动而不是复制 - 在Linux中,移动是原子操作--请检查您的操作系统),并检查锁文件是否存在。
  3. 使用目录作为“锁定”。如果您正在写入NFS,则需要使用此功能,因为NFS不支持flock()。
  4. 还可以使用进程之间的共享内存,但我从未尝试过;它非常特定于操作系统。

对于所有这些方法,您都必须使用旋转锁定(失败后重试)技术来获取和测试锁定。这确实留下了小的错误同步窗口,但通常足够小,不会成为主要问题。

如果您正在寻找跨平台的解决方案,则最好通过其他机制记录到另一个系统(上面提到的NFS技术是次佳选择)。

请注意,sqlite在NFS上也受到与普通文件相同的限制,因此无法在网络共享上写入sqlite数据库并免费同步。


4
注意:在Win32中,移动/重命名操作并非具有原子性。参考链接:https://dev59.com/I3VC5IYBdhLWcg3w1E_w - sherbang
7
自 Python 3.3 起,在 Win32 环境下,使用 os.rename 函数进行重命名操作时已经具有原子性。详见:https://bugs.python.org/issue8828 - Ghostkeeper

9

以下是使用filelock库的示例,该库类似于Evan Fossmark的实现

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:块内的任何代码都是线程安全的,这意味着在另一个进程访问该文件之前,它将完成。


4
这并不是对Evan回答的添加,而是完全与其无关,虽然您可能没有意识到这一点!令人有些困惑的是,您在答案中提供了链接到PyPI上的filelock模块(https://pypi.org/project/filelock/),该模块与Evan的模块都公开了一个`FileLock`类,但它们是完全不相关的。您可以在GitHub查看,Evan的代码位于https://github.com/dmfrey/FileLock/blob/master/filelock/filelock.py,并未与您在此处使用的https://github.com/tox-dev/py-filelock/tree/main/src/filelock的代码共享任何代码或祖先关系。 - Mark Amery
哦,哇,我甚至没有注意到。谢谢你指出来,我已经相应地更新了我的帖子 :) - Josh Correia
这是线程安全的,但不是进程安全的。 - TorokLev

7

在操作系统级别协调对单个文件的访问存在各种问题,您可能不想解决。

最好的方法是有一个单独的进程来协调对该文件的读/写访问。


21
换言之,实现一个数据库服务器,它是协调对该文件的读/写访问的单独进程。 - Eli Bendersky
1
这实际上是最好的答案。仅仅说“使用数据库服务器”过于简单化,因为数据库并不总是适合该工作。如果需要使用纯文本文件怎么办?一个好的解决方案可能是生成一个子进程,然后通过命名管道、Unix套接字或共享内存访问它。 - Brendon Crawford
17
这段话的意思是:这只是毫无解释的恐吓,因此得到负一分。像使用 flock 这样的函数一样,将文件锁定以进行写入似乎是一个非常简单明了的概念,操作系统可以提供这种功能。你们采用“自己编写互斥锁和管理它们的守护进程”的方法,似乎是一种极端而复杂的方法来解决一个问题,但你们并没有告诉我们这个问题具体是什么,只是可怕地暗示它的存在。 - Mark Amery
-1 是因为 @Mark Amery 给出的原因,同时也是因为提供了一个没有证据支持的关于 OP 想要解决哪些问题的观点。 - Michael Krebs

3

锁定文件通常是平台特定的操作,因此您可能需要考虑在不同操作系统上运行的可能性。例如:

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模块也可以用来获取关于运行平台的信息,使用platform.system()即可。http://docs.python.org/library/platform.html。 - monkut

2
这对我很有帮助: 不要占用大文件,分成几个小文件 你可以创建一个名为Temp的文件,在删除文件A后将Temp重命名为A。
import os
import json

def Server():
    i = 0
    while i == 0:
        try:        
                with open(File_Temp, "w") as file:
                    json.dump(DATA, file, indent=2)
                if os.path.exists(File_A):
                    os.remove(File_A)
                os.rename(File_Temp, File_A)
                i = 1
        except OSError as e:
                print ("file locked: " ,str(e))
                time.sleep(1)
            
            
def Clients():
    i = 0
    while i == 0:
        try:
            if os.path.exists(File_A):
                with open(File_A,"r") as file:
                    DATA_Temp = file.read()
            DATA = json.loads(DATA_Temp)
            i = 1
        except OSError as e:
            print (str(e))
            time.sleep(1)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接