49

解决这个问题的最优雅的方法是什么:

  • 打开文件进行读取,但前提是它尚未打开以进行写入
  • 打开文件进行写入,但前提是它尚未打开以进行读取或写入

内置函数是这样工作的

>>> path = r"c:\scr.txt"
>>> file1 = open(path, "w")
>>> print file1
<open file 'c:\scr.txt', mode 'w' at 0x019F88D8>
>>> file2 = open(path, "w")
>>> print file2
<open file 'c:\scr.txt', mode 'w' at 0x02332188>
>>> file1.write("111")
>>> file2.write("222")
>>> file1.close()

scr.txt 现在包含“111”。

>>> file2.close()

scr.txt 已被覆盖,现在包含“222”(在 Windows 上,Python 2.4)。

该解决方案应该在同一个进程(如上面的示例中)以及另一个进程打开文件时工作。
如果一个崩溃的程序不会保持锁打开,这是首选。

4

7 回答 7

27

我认为没有完全跨平台的方式。在 unix 上, fcntl 模块将为您执行此操作。但是在 Windows 上(我假设你是通过路径),你需要使用 win32file 模块。

幸运的是,python 食谱中有一个使用平台适当方法的可移植实现( portalocker )。

要使用它,请打开文件,然后调用:

portalocker.lock(file, flags)

其中标志是 portalocker.LOCK_EX 用于独占写入访问,或 LOCK_SH 用于共享读取访问。

于 2008-10-09T09:00:09.717 回答
12

该解决方案应该在同一个进程(如上面的示例中)以及另一个进程打开文件时工作。

如果“另一个进程”是指“任何进程”(即不是您的程序),那么在 Linux 中,仅依靠系统调用(fcntl和朋友)是无法完成此任务的。你想要的是强制锁定,而 Linux 获取它的方式有点复杂:

使用mand选项重新挂载包含您的文件的分区:

# mount -o remount,mand /dev/hdXY

为您的文件设置sgid标志:

# chmod g-x,g+s yourfile

在您的 Python 代码中,获取对该文件的排他锁:

fcntl.flock(fd, fcntl.LOCK_EX)

现在,在您释放锁之前,即使cat也无法读取文件。

于 2008-10-12T02:46:56.523 回答
7

编辑:我自己解决了!通过使用目录存在和年龄作为锁定机制!按文件锁定仅在 Windows 上是安全的(因为 Linux 会默默地覆盖),但按目录锁定在 Linux 和 Windows 上都可以完美运行。请参阅我的 GIT,我为此创建了一个易于使用的类“lockbydir.DLock”

https://github.com/drandreaskrueger/lockbydir

在自述文件的底部,您可以找到 3 个 GITplayer,您可以在其中看到在浏览器中实时执行的代码示例!很酷,不是吗?:-)

感谢您的关注


这是我最初的问题:

我想回答 parity3 ( https://meta.stackoverflow.com/users/1454536/parity3 ) 但我既不能直接发表评论('你必须有 50 声望才能发表评论'),我也看不到任何联系方式他/她直接。你有什么建议给我,让他打通?

我的问题:

我已经实现了类似于 parity3 在这里建议的答案:https ://stackoverflow.com/a/21444311/3693375 (“假设你的 Python 解释器,并且......”)

它运行良好 - 在 Windows 上。(我正在使用它来实现跨独立启动的进程工作的锁定机制。https://github.com/drandreaskrueger/lockbyfile

但除了 parity3 说的,它在 Linux 上的工作方式不同:

os.rename(src, dst)

将文件或目录 src 重命名为 dst。... 在 Unix 上,如果 dst 存在并且是一个文件,如果用户有权限,它将被静默替换。如果 src 和 dst 在不同的文件系统上,该操作可能会在某些 Unix 风格上失败。如果成功,重命名将是一个原子操作(这是 POSIX 要求)。在 Windows 上,如果 dst 已经存在,将引发 OSError ( https://docs.python.org/2/library/os.html#os.rename )

无声替换是问题所在。在 Linux 上。“如果 dst 已经存在,将引发 OSError”对我的目的来说非常有用。但遗憾的是,仅在 Windows 上。

我猜 parity3 的例子大部分时间仍然有效,因为他的 if 条件

if not os.path.exists(lock_filename):
    try:
        os.rename(tmp_filename,lock_filename)

但是整个事情不再是原子的了。

因为 if 条件可能在两个并行进程中为真,然后两者都会重命名,但只有一个会赢得重命名竞赛。并且没有引发异常(在 Linux 中)。

有什么建议么?谢谢!

PS:我知道这不是正确的方法,但我缺乏替代方法。请不要以降低我的声誉来惩罚我。我环顾四周,自己解决这个问题。如何在这里 PM 用户?我为什么不能呢?

于 2015-02-15T23:41:16.677 回答
4

这是一个可移植实现的 win32 部分的开始,它不需要单独的锁定机制。

需要Python for Windows Extensions才能使用 win32 api,但这对于 windows 上的 python 来说已经是强制性的了,也可以使用ctypes来完成。如果需要,可以调整代码以公开更多功能(例如允许FILE_SHARE_READ而不是根本不共享)。另请参阅 MSDN 文档以了解CreateFileWriteFile系统调用,以及有关创建和打开文件的文章

如前所述,如果需要,您可以使用标准fcntl模块来实现 unix 的一半。

import winerror, pywintypes, win32file

class LockError(StandardError):
    pass

class WriteLockedFile(object):
    """
    Using win32 api to achieve something similar to file(path, 'wb')
    Could be adapted to handle other modes as well.
    """
    def __init__(self, path):
        try:
            self._handle = win32file.CreateFile(
                path,
                win32file.GENERIC_WRITE,
                0,
                None,
                win32file.OPEN_ALWAYS,
                win32file.FILE_ATTRIBUTE_NORMAL,
                None)
        except pywintypes.error, e:
            if e[0] == winerror.ERROR_SHARING_VIOLATION:
                raise LockError(e[2])
            raise
    def close(self):
        self._handle.close()
    def write(self, str):
        win32file.WriteFile(self._handle, str)

以下是您上面的示例的行为方式:

>>> path = "C:\\scr.txt"
>>> file1 = WriteLockedFile(path)
>>> file2 = WriteLockedFile(path) #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
    ...
LockError: ...
>>> file1.write("111")
>>> file1.close()
>>> print file(path).read()
111
于 2008-10-09T19:30:33.970 回答
3

我更喜欢使用filelock,这是一个几乎不需要任何额外代码的跨平台 Python 库。以下是如何使用它的示例:

from filelock import FileLock

lockfile = r"c:\scr.txt"
lock = FileLock(lockfile + ".lock")
with lock:
    file = open(path, "w")
    file.write("111")
    file.close()

块中的任何代码with lock:都是线程安全的,这意味着它将在另一个进程访问该文件之前完成。

于 2020-01-17T21:50:28.297 回答
2

假设您的 Python 解释器,以及底层操作系统和文件系统将 os.rename 视为原子操作,并且当目标存在时会出错,则以下方法没有竞争条件。我在 linux 机器上的生产中使用它。不需要第三方库并且不依赖于操作系统,并且除了创建额外的文件之外,对于许多用例来说,性能损失是可以接受的。您可以在这里轻松应用 python 的函数装饰器模式或“with_statement”上下文管理器来抽象出混乱。

在新进程/任务开始之前,您需要确保 lock_filename 不存在。

import os,time
def get_tmp_file():
    filename='tmp_%s_%s'%(os.getpid(),time.time())
    open(filename).close()
    return filename

def do_exclusive_work():
    print 'exclusive work being done...'

num_tries=10
wait_time=10
lock_filename='filename.lock'
acquired=False
for try_num in xrange(num_tries):
    tmp_filename=get_tmp_file()
    if not os.path.exists(lock_filename):
        try:
            os.rename(tmp_filename,lock_filename)
            acquired=True
        except (OSError,ValueError,IOError), e:
            pass
    if acquired:
        try:
            do_exclusive_work()
        finally:
            os.remove(lock_filename)
        break
    os.remove(tmp_filename)
    time.sleep(wait_time)
assert acquired, 'maximum tries reached, failed to acquire lock file'

编辑

os.rename 会默默地覆盖非 Windows 操作系统上的目的地。感谢您指出这一点@akrueger!

这是一个解决方法,从这里收集:

您可以使用以下命令代替使用 os.rename:

try:
    if os.name != 'nt': # non-windows needs a create-exclusive operation
        fd = os.open(lock_filename, os.O_WRONLY | os.O_CREAT | os.O_EXCL)
        os.close(fd)
    # non-windows os.rename will overwrite lock_filename silently.
    # We leave this call in here just so the tmp file is deleted but it could be refactored so the tmp file is never even generated for a non-windows OS
    os.rename(tmp_filename,lock_filename)
    acquired=True
except (OSError,ValueError,IOError), e:
    if os.name != 'nt' and not 'File exists' in str(e): raise

@akrueger 您可能对基于目录的解决方案很好,只是为您提供了另一种方法。

于 2014-01-29T22:45:22.670 回答
0

为了确保您在一个应用程序中打开文件时的安全,您可以尝试以下操作:

import time
class ExclusiveFile(file):
    openFiles = {}
    fileLocks = []

    class FileNotExclusiveException(Exception):
        pass

    def __init__(self, *args):

        sMode = 'r'
        sFileName = args[0]
        try:
            sMode = args[1]
        except:
            pass
        while sFileName in ExclusiveFile.fileLocks:
            time.sleep(1)

        ExclusiveFile.fileLocks.append(sFileName)

        if not sFileName in ExclusiveFile.openFiles.keys() or (ExclusiveFile.openFiles[sFileName] == 'r' and sMode == 'r'):
            ExclusiveFile.openFiles[sFileName] = sMode
            try:
                file.__init__(self, sFileName, sMode)
            finally:
                ExclusiveFile.fileLocks.remove(sFileName)
         else:
            ExclusiveFile.fileLocks.remove(sFileName)
            raise self.FileNotExclusiveException(sFileName)

    def close(self):
        del ExclusiveFile.openFiles[self.name]
        file.close(self)

这样你就可以继承file类。现在只需执行以下操作:

>>> f = ExclusiveFile('/tmp/a.txt', 'r')
>>> f
<open file '/tmp/a.txt', mode 'r' at 0xb7d7cc8c>
>>> f1 = ExclusiveFile('/tmp/a.txt', 'r')
>>> f1
<open file '/tmp/a.txt', mode 'r' at 0xb7d7c814>
>>> f2 = ExclusiveFile('/tmp/a.txt', 'w') # can't open it for writing now
exclfile.FileNotExclusiveException: /tmp/a.txt

如果您首先使用“w”模式打开它,它将不再允许打开,即使在读取模式下,正如您想要的那样......

于 2008-10-09T07:48:44.493 回答