Python在Linux上的系统范围互斥锁

77

有没有一种简单的方法在Linux上使用Python实现系统范围内的互斥锁?所谓的“系统范围内”,是指互斥锁将被一组Python进程使用;这与传统的互斥锁不同,后者由同一进程内的一组线程使用。

编辑:我不确定Python的multiprocessing包是否是我需要的。例如,我可以在两个不同的解释器中执行以下操作:

from multiprocessing import Lock
L = Lock()
L.acquire()

当我在两个独立的解释器中同时执行这些命令时,我希望其中一个会挂起。然而,现在两个都没有挂起;看起来它们没有获取到相同的互斥锁。

https://dev59.com/EW025IYBdhLWcg3w252y - Anycorn
6个回答

48

“传统”的Unix解决方案是使用文件锁。您可以使用lockf(3)锁定文件的部分,以便其他进程无法编辑它;一个非常常见的滥用情况是将其用作进程之间的互斥量。Python等价物是fcntl.lockf

传统上,您需要将锁定进程的PID写入锁定文件中,以便可以识别和修复因进程在持有锁时死亡而导致的死锁问题。

这样可以实现您想要的效果,因为您的锁在全局命名空间(文件系统)中,并且可供所有进程访问。这种方法的好处还在于非Python程序可以参与您的锁定。缺点是您需要一个地方来存放此锁定文件;另外,某些文件系统实际上无法正确地进行锁定,因此存在无法实现排除的风险。有得必有失。


2
锁文件的逻辑位置是/var/lock,但是如果有大量的锁定操作,我建议使用/tmp,因为并非所有系统都在tmpfs内存盘中拥有/var/lock - Kimvais
并非所有系统都将/tmp存储在tmpfs内存磁盘中;我的OS X安装似乎没有。尽管如此,这些都是很好的观点。 - zmccord
2
哪些文件系统无法正确锁定?或者,我在哪里可以找到关于哪些文件系统无法正确锁定的信息? - cowlinator
将锁定进程的PID写入锁定文件非常困难,因为w+(读取、写入、如果存在则截断、如果不存在则创建)会在锁定尝试之前截断文件,而a+(读取、追加写入、如果不存在则创建)不允许在某些Linux上进行seek(0)。 - Ben Slade
https://dev59.com/LV4b5IYBdhLWcg3whCCV 解决了这些问题。 - zmccord
显示剩余2条评论

21

我的回答与其他回答有重叠之处,但为了添加一些人们可以复制粘贴的内容,我经常做这样的事情。

class Locker:
    def __enter__ (self):
        self.fp = open("./lockfile.lck")
        fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX)

    def __exit__ (self, _type, value, tb):
        fcntl.flock(self.fp.fileno(), fcntl.LOCK_UN)
        self.fp.close()

然后将其用作:

print("waiting for lock")
with Locker():
    print("obtained lock")
    time.sleep(5.0)

要进行测试,执行 touch lockfile.lck,然后在两个或多个不同的终端中运行上面的代码(从相同的目录中运行)。

更新:smwikipedia提到我的解决方案是Unix特定的。最近我需要一个便携式版本,并从一个随机的github项目中提出了以下想法。我不确定是否需要seek()调用,但它们存在是因为Windows API锁定文件中的特定位置。如果您除了锁定之外不使用该文件,则可能可以删除这些seeks。

if os.name == "nt":
    import msvcrt

    def portable_lock(fp):
        fp.seek(0)
        msvcrt.locking(fp.fileno(), msvcrt.LK_LOCK, 1)

    def portable_unlock(fp):
        fp.seek(0)
        msvcrt.locking(fp.fileno(), msvcrt.LK_UNLCK, 1)
else:
    import fcntl

    def portable_lock(fp):
        fcntl.flock(fp.fileno(), fcntl.LOCK_EX)

    def portable_unlock(fp):
        fcntl.flock(fp.fileno(), fcntl.LOCK_UN)


class Locker:
    def __enter__(self):
        self.fp = open("./lockfile.lck")
        portable_lock(self.fp)

    def __exit__(self, _type, value, tb):
        portable_unlock(self.fp)
        self.fp.close()

3
我倾向于首先由超级用户创建文件,这样个别工作人员就无法删除它。我还想避免在任何工作人员开始之前,具有更高权限的东西来编写文件,可能会拒绝他们的访问。我认为创建该文件的工作归我的“安装程序”(缺乏更好的词语),我更喜欢在安装时查看该文件创建的任何问题,而不是在运行时难以调试。但如果对您的应用程序有用,请使用它! - Keeely
这似乎是特定于类Unix系统的。 - smwikipedia
@smwikipedia 这个问题是针对Linux的。在Windows上,你有一个全局命名空间来管理互斥对象,所以你可以使用CreateMutex来创建一个命名互斥对象,然后使用WaitForSingleObject等待它。这是我从我的C++时代记得的,我认为这些函数在Python win32扩展中也是可用的(尽管我到目前为止从未在Python中需要过这个)。 - Keeely
这是我能找到的Python中唯一真正的全局锁。 如果有人不想创建/删除所需的锁文件,则可以在__enter__中添加open(“./ lockfile.lock”,“wb”),并在__exit__中添加os.remove(“./ lockfile.lock”),并且它应该起到相同的作用。 谢谢! - Mattkwish
1
@Mattkwish 谢谢,但我建议你不要这样做。移除操作在任何锁之外都可能发生,并且如果它在打开后立即发生,则该文件将从公共文件系统中删除,但与私有文件的句柄相关联的锁定仍然存在。因此,您将有两个不同的文件在使用中,它不再是全局的。 - Keeely
显示剩余4条评论

20

尝试使用 ilock 库:

from ilock import ILock

with ILock('Unique lock name'):
    # The code should be run as a system-wide single instance
    ...

@KT。TemporaryFileLock存在竞争问题。此外,ILock支持重入。 - Symon
2
如果文件被删除,可能会出现意外异常。 - Symon
我明白了,你在提到代码中的“WindowsError”捕获部分。那么将这个try-catch添加到portalocker中可能是有意义的。如果你再添加一个TemporaryFileRLock(只需要10行代码),那么维护一个单独的库的需求可能就会消失。 - KT.
@KT。使用portalocker,我可以指定代码是否应该阻塞,如果锁被占用或引发异常吗?是否有一个简单的API可以在没有上下文管理器的情况下使用锁?我可以指定非排他性读锁以及排他性写锁吗? - Konstantin Schubert
@KonstantinSchubert 所有文档都在这里:http://portalocker.readthedocs.io/en/latest/ - KT.
显示剩余2条评论

13

POSIX标准指定了可用于此目的的进程间信号量。 http://linux.die.net/man/7/sem_overview

Python中的multiprocessing模块是建立在此API和其他API之上的。特别是,multiprocessing.Lock提供了跨进程的“互斥锁”。 http://docs.python.org/library/multiprocessing.html#synchronization-between-processes

编辑以回应编辑后的问题:

在您的概念验证中,每个进程都会构造一个Lock()。因此,您将拥有两个单独的锁。这就是为什么没有一个进程等待的原因。您需要在进程之间共享同一个锁。我在multiprocessing文档中链接的部分说明了如何实现该功能。


谢谢,但是“多进程”似乎不是我需要的;请参见编辑后的问题。 - emchristiansen
33
您提供的链接中展示了一个主进程如何生成10个子进程,并向每个生成的子进程传递一个锁对象。我的用例不同,因为没有主进程生成子进程。在我的情况下,每个进程完全独立地调用,但是它们仍然需要进行协调。 - emchristiansen
3
如果对等方之间没有关联但仍需要共享互斥锁,那么具有配置数字地址的共享内存可能是唯一的选择。然后,互斥锁对象可以存在于共享内存段中。可能没有适用于此的Python API;如果没有,您可能需要使用本地编程语言。请确认PThreads完全支持此用例;我担心它可能不支持。对我来说,这是一个设计上的缺陷;似乎应该使用线程和互斥锁,或者使用像redis或riak这样的单独进程进行仲裁。 - wberry

6

仅需将 posix_ipc 库的 Semaphore 类加入列表即可。

计数为1的 Semaphore 可用作 Mutex。 为了完成线程三元组,SystemEvent 库利用了 posix_ipc 并提供了一个 Event

此外,我还要指出这不会轮询硬盘!


1
从另一个来源,我也发现这个库对于原始问题最有用。在我的情况下:2-3-4个Python程序(相同的程序,使用不同的参数启动2-3-4次) - 它们必须使用非线程安全资源。 POSIX_IPC是迄今为止最简单的解决方案。不需要涉及文件系统相关的事情。 - V-Mark

1

如果需要对绝对独立的进程进行同步(例如,包括不属于同一进程树的Linux进程),可以使用fcntl.flock来实现系统范围的互斥。我认为在Linux的/run/shm文件夹下使用内存文件可能会使其性能更快。

更多信息请参见此处


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