确保只运行一个程序实例

时间:2008-12-19 12:42:53

标签: python process locking mutual-exclusion

是否有Pythonic方法只运行一个程序实例?

我提出的唯一合理的解决方案是尝试在某个端口上将其作为服务器运行,然后第二个程序尝试绑定到同一个端口 - 失败。但这不是一个好主意,也许有比这更轻巧的东西?

(考虑到程序有时会失败,即段错误 - 所以像“锁定文件”这样的东西不起作用)

更新:提供的解决方案比仅存在一个不存在服务器的端口要复杂得多且依赖性要小得多,所以我必须选择那个。

24 个答案:

答案 0 :(得分:88)

以下代码应该完成这项工作,它是跨平台的,可以在Python 2.4-3.2上运行。我在Windows,OS X和Linux上测试过它。

from tendo import singleton
me = singleton.SingleInstance() # will sys.exit(-1) if other instance is running

最新的代码版本可用singleton.py。请file bugs here

您可以使用以下方法之一安装tend:

答案 1 :(得分:34)

简单的跨平台解决方案,位于another question zgoda 中:

import fcntl, sys
pid_file = 'program.pid'
fp = open(pid_file, 'w')
try:
    fcntl.lockf(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
except IOError:
    # another instance is running
    sys.exit(0)

很像S.Lott的建议,但有代码。

答案 2 :(得分:25)

此代码特定于Linux。它使用“抽象”UNIX域套接字,但它很简单,不会留下陈旧的锁定文件。我更喜欢上面的解决方案,因为它不需要专门保留的TCP端口。

try:
    import socket
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    ## Create an abstract socket, by prefixing it with null. 
    s.bind( '\0postconnect_gateway_notify_lock') 
except socket.error as e:
    error_code = e.args[0]
    error_string = e.args[1]
    print "Process already running (%d:%s ). Exiting" % ( error_code, error_string) 
    sys.exit (0) 

可以更改唯一字符串postconnect_gateway_notify_lock以允许需要强制执行单个实例的多个程序。

答案 3 :(得分:20)

我不知道它是否足够pythonic,但在Java世界中,侦听定义的端口是一个非常广泛使用的解决方案,因为它适用于所有主要平台,并且没有任何崩溃程序的问题。

侦听端口的另一个好处是可以向正在运行的实例发送命令。例如,当用户第二次启动程序时,您可以向正在运行的实例发送一个命令,告诉它打开另一个窗口(例如,这就是Firefox所做的。我不知道他们是否使用TCP端口或命名管道或类似的东西,'虽然)。

答案 4 :(得分:11)

之前从未编写过python,但这是我刚刚在mycheckpoint中实现的,以防止它被crond启动两次或更多次:

import os
import sys
import fcntl
fh=0
def run_once():
    global fh
    fh=open(os.path.realpath(__file__),'r')
    try:
        fcntl.flock(fh,fcntl.LOCK_EX|fcntl.LOCK_NB)
    except:
        os._exit(0)

run_once()

在另一期(http://stackoverflow.com/questions/2959474)中发布此事后,发现了Slava-N的建议。这个被称为函数,锁定执行脚本文件(不是pid文件)并保持锁定,直到脚本结束(正常或错误)。

答案 5 :(得分:10)

使用pid文件。你有一些已知的位置,“/ path / to / pidfile”,在启动时你会做这样的事情(部分伪代码,因为我是预先咖啡,不想那么努力):

import os, os.path
pidfilePath = """/path/to/pidfile"""
if os.path.exists(pidfilePath):
   pidfile = open(pidfilePath,"r")
   pidString = pidfile.read()
   if <pidString is equal to os.getpid()>:
      # something is real weird
      Sys.exit(BADCODE)
   else:
      <use ps or pidof to see if the process with pid pidString is still running>
      if  <process with pid == 'pidString' is still running>:
          Sys.exit(ALREADAYRUNNING)
      else:
          # the previous server must have crashed
          <log server had crashed>
          <reopen pidfilePath for writing>
          pidfile.write(os.getpid())
else:
    <open pidfilePath for writing>
    pidfile.write(os.getpid())

因此,换句话说,您正在检查是否存在pid文件;如果没有,请将您的pid写入该文件。如果pidfile确实存在,那么检查pid是否是正在运行的进程的pid;如果是这样,那么你有另一个正在运行的进程,所以只需关闭。如果没有,那么前一个进程崩溃,所以记录它,然后将自己的pid写入文件而不是旧文件。然后继续。

答案 6 :(得分:6)

你已经在另一个帖子中找到了对类似问题的回复,所以为了完整起见,请看看如何在Windows上使用名为mutex的方法来实现相同的目标。

http://code.activestate.com/recipes/474070/

答案 7 :(得分:5)

这可能有效。

  1. 尝试将PID文件创建到已知位置。如果你失败了,有人将文件锁定,你就完成了。

  2. 正常完成后,请关闭并删除PID文件,以便其他人可以覆盖它。

  3. 即使程序崩溃,您也可以将程序包装在一个删除PID文件的shell脚本中。

    如果程序挂起,您也可以使用PID文件终止程序。

答案 8 :(得分:3)

在unix上使用锁文件是一种非常常见的方法。如果崩溃,您必须手动清理。您可以在文件中存储PID,并在启动时检查是否存在具有此PID的进程,否则覆盖锁定文件。 (但是,您还需要锁定read-file-check-pid-rewrite-file)。您将在os - 包中找到获取和检查pid所需的内容。检查是否存在具有给定pid的进程的常用方法是向其发送非致命信号。

其他替代方案可能是将它与flock或posix信号量结合起来。

像saua提议的那样,打开一个网络套接字可能是最容易和最便携的。

答案 9 :(得分:3)

对于使用 wxPython 作为其应用程序的任何人,您可以使用函数wx.SingleInstanceChecker documented here

我个人使用wx.App的子类,它使用wx.SingleInstanceChecker并从False返回OnInit(),如果现有的应用实例已经执行如下:

import wx

class SingleApp(wx.App):
    """
    class that extends wx.App and only permits a single running instance.
    """

    def OnInit(self):
        """
        wx.App init function that returns False if the app is already running.
        """
        self.name = "SingleApp-%s".format(wx.GetUserId())
        self.instance = wx.SingleInstanceChecker(self.name)
        if self.instance.IsAnotherRunning():
            wx.MessageBox(
                "An instance of the application is already running", 
                "Error", 
                 wx.OK | wx.ICON_WARNING
            )
            return False
        return True

这是禁止多个实例的wx.App的简单替代品。要使用它,只需在代码中将wx.App替换为SingleApp,如下所示:

app = SingleApp(redirect=False)
frame = wx.Frame(None, wx.ID_ANY, "Hello World")
frame.Show(True)
app.MainLoop()

答案 10 :(得分:2)

我发布这个作为答案,因为我是新用户,Stack Overflow不会让我投票。

Sorin Sbarnea的解决方案适用于OS X,Linux和Windows,我很感激。

但是,tempfile.gettempdir()在OS X和Windows下运行一种方式,在另一些/多/全(?)* nix下运行另一种方式(忽略OS X也是Unix的事实!)。差异对此代码很重要。

OS X和Windows具有特定于用户的临时目录,因此一个用户创建的临时文件对另一个用户不可见。相比之下,在许多版本的* nix(我测试了Ubuntu 9,RHEL 5,OpenSolaris 2008和FreeBSD 8)下,所有用户的临时目录都是/ tmp。

这意味着当在多用户计算机上创建锁文件时,它在/ tmp中创建,并且只有第一次创建锁文件的用户才能运行该应用程序。

一种可能的解决方案是将当前用户名嵌入锁定文件的名称中。

值得注意的是,OP的抓取端口的解决方案也会在多用户计算机上行为不端。

答案 11 :(得分:2)

我在我的gentoo上使用single_process;

pip install single_process

示例

from single_process import single_process

@single_process
def main():
    print 1

if __name__ == "__main__":
    main()   

参考:https://pypi.python.org/pypi/single_process/1.0

答案 12 :(得分:2)

这是我最终的Windows解决方案。将以下内容放入一个模块中,也许称为“onlyone.py”,或者其他什么。将该模块直接包含在__ main __ python脚本文件中。

import win32event, win32api, winerror, time, sys, os
main_path = os.path.abspath(sys.modules['__main__'].__file__).replace("\\", "/")

first = True
while True:
        mutex = win32event.CreateMutex(None, False, main_path + "_{<paste YOUR GUID HERE>}")
        if win32api.GetLastError() == 0:
            break
        win32api.CloseHandle(mutex)
        if first:
            print "Another instance of %s running, please wait for completion" % main_path
            first = False
        time.sleep(1)

说明

代码尝试创建一个名称来源于脚本完整路径的互斥锁。我们使用正斜杠来避免与真实文件系统的混淆。

优点

  • 无需配置或“魔术”标识符,可根据需要在多个不同的脚本中使用它。
  • 没有陈旧的文件,互斥体会随你而死。
  • 等待
  • 时打印有用的信息

答案 13 :(得分:1)

这是我使用 Python 3.7.9 在 Windows Server 2016 和 Ubuntu 20.04 上测试过的 cross platform example

import os

class SingleInstanceChecker:
    def __init__(self, id):
        if isWin():
            ensure_win32api()
            self.mutexname = id
            self.lock = win32event.CreateMutex(None, False, self.mutexname)
            self.running = (win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS)

        else:
            ensure_fcntl()
            self.lock = open(f"/tmp/isnstance_{id}.lock", 'wb')
            try:
                fcntl.lockf(self.lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
                self.running = False
            except IOError:
                self.running = True


    def already_running(self):
        return self.running
        
    def __del__(self):
        if self.lock:
            try:
                if isWin():
                    win32api.CloseHandle(self.lock)
                else:
                    os.close(self.lock)
            except Exception as ex:
                pass

# ---------------------------------------
# Utility Functions
# Dynamically load win32api on demand
# Install with: pip install pywin32
win32api=winerror=win32event=None
def ensure_win32api():
    global win32api,winerror,win32event
    if win32api is None:
        import win32api
        import winerror
        import win32event


# Dynamically load fcntl on demand
# Install with: pip install fcntl
fcntl=None
def ensure_fcntl():
    global fcntl
    if fcntl is None:
        import fcntl


def isWin():
    return (os.name == 'nt')
# ---------------------------------------

这里正在使用:

import time, sys

def main(argv):
    _timeout = 10
    print("main() called. sleeping for %s seconds" % _timeout)
    time.sleep(_timeout)
    print("DONE")


if __name__ == '__main__':
    SCR_NAME = "my_script"
    sic = SingleInstanceChecker(SCR_NAME)
    if sic.already_running():
        print("An instance of {} is already running.".format(SCR_NAME))
        sys.exit(1)
    else:
        main(sys.argv[1:])

答案 14 :(得分:1)

迟到的答案,但对于 Windows,您可以使用:

from win32event import CreateMutex
from win32api import CloseHandle, GetLastError
from winerror import ERROR_ALREADY_EXISTS
import sys

class singleinstance:
    """ Limits application to single instance """

    def __init__(self):
        self.mutexname = "testmutex_{D0E858DF-985E-4907-B7FB-8D732C3FC3B9}"
        self.mutex = CreateMutex(None, False, self.mutexname)
        self.lasterror = GetLastError()
    
    def alreadyrunning(self):
        return (self.lasterror == ERROR_ALREADY_EXISTS)
        
    def __del__(self):
        if self.mutex:
            CloseHandle(self.mutex)

用法

# do this at beginnig of your application
myapp = singleinstance()

# check is another instance of same program running
if myapp.alreadyrunning():
    print ("Another instance of this program is already running")
    sys.exit(1)

答案 15 :(得分:1)

在Windows上最好的解决方案是使用@zgoda建议的互斥锁。

import win32event
import win32api
from winerror import ERROR_ALREADY_EXISTS

mutex = win32event.CreateMutex(None, False, 'name')
last_error = win32api.GetLastError()

if last_error == ERROR_ALREADY_EXISTS:
   print("App instance already running")

某些答案使用fctnl(也包含在@sorin tento软件包中),该软件包在Windows上不可用,如果您尝试使用pyinstaller之类的软件包进行静态导入来冻结python应用程序,它会引发错误。

此外,使用锁定文件方法还会对数据库文件造成read-only问题(sqlite3会遇到此问题)。

答案 16 :(得分:0)

这是一个跨平台的实现,使用上下文管理器创建一个临时锁定文件。

可用于管理多个任务。

import os
from contextlib import contextmanager
from time import sleep


class ExceptionTaskInProgress(Exception):
    pass


# Context manager for suppressing exceptions
class SuppressException:
    def __init__(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, *exc):
        return True


# Context manager for task
class TaskSingleInstance:
    def __init__(self, task_name, lock_path):
        self.task_name = task_name
        self.lock_path = lock_path
        self.lock_filename = os.path.join(self.lock_path, self.task_name + ".lock")

        if os.path.exists(self.lock_filename):
            raise ExceptionTaskInProgress("Resource already in use")

    def __enter__(self):
        self.fl = open(self.lock_filename, "w")
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.fl.close()
        os.unlink(self.lock_filename)


# Here the task is silently interrupted
# if it is already running on another instance.
def main1():
    task_name = "task1"
    tmp_filename_path = "."
    with SuppressException():
        with TaskSingleInstance(task_name, tmp_filename_path):
            print("The task `{}` has started.".format(task_name))
            # The single task instance code is here.
            sleep(5)
            print("The task `{}` has completed.".format(task_name))


# Here the task is interrupted with a message
# if it is already running in another instance.
def main2():
    task_name = "task1"
    tmp_filename_path = "."
    try:
        with TaskSingleInstance(task_name, tmp_filename_path):
            print("The task `{}` has started.".format(task_name))
            # The single task instance code is here.
            sleep(5)
            print("Task `{}` completed.".format(task_name))
    except ExceptionTaskInProgress as ex:
        print("The task `{}` is already running.".format(task_name))


if __name__ == "__main__":
    main1()
    main2()

答案 17 :(得分:0)

这是一个带有 contextmanager 和 memcached 的 django 的好例子: https://docs.celeryproject.org/en/latest/tutorials/task-cookbook.html

可用于保护不同主机上的同时操作。 可用于管理多个任务。 也可以针对简单的 python 脚本进行更改。

我对上面代码的修改如下:

import time
from contextlib import contextmanager
from django.core.cache import cache


@contextmanager
def memcache_lock(lock_key, lock_value, lock_expire):
    timeout_at = time.monotonic() + lock_expire - 3

    # cache.add fails if the key already exists
    status = cache.add(lock_key, lock_value, lock_expire)
    try:
        yield status
    finally:
        # memcache delete is very slow, but we have to use it to take
        # advantage of using add() for atomic locking
        if time.monotonic() < timeout_at and status:
            # don't release the lock if we exceeded the timeout
            # to lessen the chance of releasing an expired lock owned by someone else
            # also don't release the lock if we didn't acquire it
            cache.delete(lock_key)


LOCK_EXPIRE = 60 * 10  # Lock expires in 10 minutes


def main():
    lock_name, lock_value = "lock_1", "locked"
    with memcache_lock(lock_name, lock_value, LOCK_EXPIRE) as acquired:
        if acquired:
            # single instance code here:
            pass


if __name__ == "__main__":
    main()

答案 18 :(得分:0)

在罗伯托·罗萨里奥(Roberto Rosario)的回答的基础上,我提出了以下功能:

SOCKET = None
def run_single_instance(uniq_name):
    try:
        import socket
        global SOCKET
        SOCKET = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        ## Create an abstract socket, by prefixing it with null.
        # this relies on a feature only in linux, when current process quits, the
        # socket will be deleted.
        SOCKET.bind('\0' + uniq_name)
        return True
    except socket.error as e:
        return False

我们需要定义全局SOCKET变量,因为只有在整个过程退出时才会对其进行垃圾回收。如果我们在函数中声明一个局部变量,则该变量将在函数退出后超出范围,因此将删除套接字。

所有的功劳都应该归功于罗伯托·罗萨里奥(Roberto Rosario),因为我只是澄清和阐述了他的代码。而且此代码仅在Linux上有效,如https://troydhanson.github.io/network/Unix_domain_sockets.html中以下引用的文字所述:

  

Linux具有一项特殊功能:如果UNIX域套接字的路径名   以空字节\ 0开头,其名称未映射到   文件系统。因此,它不会与文件系统中的其他名称冲突。   另外,当服务器关闭其UNIX域侦听套接字时,   抽象名称空间,其文件被删除;与常规UNIX域   套接字,文件在服务器关闭后仍然存在。

答案 19 :(得分:0)

在Linux系统上,您可能还会问 pgrep -a用于获取实例数,脚本 在进程列表中找到(选项-a显示了 完整的命令行字符串)。例如

import os
import sys
import subprocess

procOut = subprocess.check_output( "/bin/pgrep -u $UID -a python", shell=True, 
                                   executable="/bin/bash", universal_newlines=True)

if procOut.count( os.path.basename(__file__)) > 1 :        
    sys.exit( ("found another instance of >{}<, quitting."
              ).format( os.path.basename(__file__)))

如果限制适用于所有用户,请删除-u $UID。 免责声明:a)假定脚本的(基本)名称是唯一的,b)可能存在竞争条件。

答案 20 :(得分:0)

linux示例

此方法基于在关闭应用程序后自动删除临时文件的创建。 程序启动我们验证文件的存在; 如果文件存在(有待执行),则程序关闭;否则它会创建文件并继续执行程序。

from tempfile import *
import time
import os
import sys


f = NamedTemporaryFile( prefix='lock01_', delete=True) if not [f  for f in     os.listdir('/tmp') if f.find('lock01_')!=-1] else sys.exit()

YOUR CODE COMES HERE

答案 21 :(得分:0)

上周我遇到了这个确切的问题,虽然我找到了一些很好的解决方案,但我决定制作一个非常简单干净的python包并将其上传到PyPI。它与tendo的不同之处在于它可以锁定任何字符串资源名称。虽然你当然可以锁定__file__以达到同样的效果。

安装时使用:pip install quicklock

使用它非常简单:

[nate@Nates-MacBook-Pro-3 ~/live] python
Python 2.7.6 (default, Sep  9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from quicklock import singleton
>>> # Let's create a lock so that only one instance of a script will run
...
>>> singleton('hello world')
>>>
>>> # Let's try to do that again, this should fail
...
>>> singleton('hello world')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/nate/live/gallery/env/lib/python2.7/site-packages/quicklock/quicklock.py", line 47, in singleton
    raise RuntimeError('Resource <{}> is currently locked by <Process {}: "{}">'.format(resource, other_process.pid, other_process.name()))
RuntimeError: Resource <hello world> is currently locked by <Process 24801: "python">
>>>
>>> # But if we quit this process, we release the lock automatically
...
>>> ^D
[nate@Nates-MacBook-Pro-3 ~/live] python
Python 2.7.6 (default, Sep  9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from quicklock import singleton
>>> singleton('hello world')
>>>
>>> # No exception was thrown, we own 'hello world'!

看看:https://pypi.python.org/pypi/quicklock

答案 22 :(得分:0)

我一直怀疑使用进程组应该有一个很好的POSIXy解决方案,而不必点击文件系统,但我无法确定它。类似的东西:

启动时,您的进程会向特定组中的所有进程发送“kill -0”。如果存在任何此类过程,则退出。然后它加入了小组。没有其他进程使用该组。

然而,这有一个竞争条件 - 多个进程都可以在同一时间完成此操作,并且所有进程最终加入组并同时运行。当您添加某种互斥锁以使其不透水时,您不再需要进程组。

如果您的流程仅由cron启动,每分钟或每小时一次,这可能是可以接受的,但这让我有点紧张,因为在您不希望它的那一天它会出错。

我想这毕竟不是一个很好的解决方案,除非有人能改进它吗?

答案 23 :(得分:-1)

import sys,os

# start program
try:  # (1)
    os.unlink('lock')  # (2)
    fd=os.open("lock", os.O_CREAT|os.O_EXCL) # (3)  
except: 
    try: fd=os.open("lock", os.O_CREAT|os.O_EXCL) # (4) 
    except:  
        print "Another Program running !.."  # (5)
        sys.exit()  

# your program  ...
# ...

# exit program
try: os.close(fd)  # (6)
except: pass
try: os.unlink('lock')  
except: pass
sys.exit()