Python脚本能否检测到另一个实例在运行并与之通信?

14 投票
4 回答
4447 浏览
提问于 2025-04-15 23:19

我想要防止同一个长时间运行的 Python 命令行脚本同时启动多个实例,并且希望新的实例能够在退出之前把数据发送给原来的实例。请问我该如何在不同操作系统上实现这个功能呢?

具体来说,我希望实现以下行为:

  1. 从命令行启动“foo.py”,它会长时间运行——可能是几天或几周,直到机器重启或者父进程把它杀掉。
  2. 每隔几分钟,同样的脚本会再次启动,但这次会使用不同的命令行参数。
  3. 当脚本启动时,它应该检查是否有其他实例正在运行。
  4. 如果有其他实例在运行,那么第二个实例应该把它的命令行参数发送给第一个实例,然后退出。
  5. 第一个实例如果收到了来自另一个脚本的命令行参数,应该启动一个新线程,并根据上一步发送的参数开始执行第二个实例本来要执行的工作。

所以我在寻找两个东西:一个是 Python 程序如何知道自己有其他实例在运行,另一个是一个 Python 命令行程序如何与另一个进行通信。

更复杂的是,这个脚本需要在 Windows 和 Linux 上都能运行,所以理想情况下解决方案只使用 Python 的标准库,而不调用特定于操作系统的功能。不过,如果我需要在代码中写一个大的 if 语句来选择 Windows 路径和 *nix 路径,那也没问题,只要“同一份代码”的解决方案不可行。

我意识到我可能可以用基于文件的方法来解决(比如第一个实例监视一个目录的变化,每个实例在想要工作时往那个目录放一个文件),但我有点担心在机器非正常关机后如何清理那些文件。我理想中是能使用内存中的解决方案。不过,如果持久化的文件方法是唯一的解决方案,我也愿意接受。

更多细节:我之所以想这样做,是因为我们的服务器使用了一种监控工具,它支持运行 Python 脚本来收集监控数据(例如数据库查询结果或网络服务调用),然后监控工具会对这些数据进行索引以备后用。这些脚本启动成本很高,但启动后运行成本低(例如,建立数据库连接和执行查询的成本)。所以我们选择让它们在无限循环中运行,直到父进程杀掉它们。

这个方法很好,但在大型服务器上,可能会有 100 个相同脚本的实例在运行,即使它们每 20 分钟才收集一次数据。这会对内存、数据库连接限制等造成很大影响。我们希望从 100 个进程和 1 个线程,转变为 1 个进程和 100 个线程,每个线程执行之前一个脚本所做的工作。

但是,改变监控工具调用脚本的方式是不可能的。我们需要保持调用方式不变(用不同的命令行参数启动一个进程),但要修改脚本,使其能够识别另一个实例正在运行,并让“新”脚本把它的工作指令(来自命令行参数)发送给“旧”脚本。

顺便说一下,我并不想在单个脚本的基础上做这个。相反,我想把这种行为打包成一个库,供许多脚本作者使用——我的目标是让脚本作者能够编写简单的单线程脚本,而不需要关心多实例的问题,并在背后处理多线程和单实例的逻辑。

4 个回答

1

也许可以试试用套接字来进行通信?

9

一般来说,启动脚本时,会建立一个通信通道,这个通道是独占的,也就是说,如果其他脚本尝试建立同样的通道,会以可预测的方式失败。这样,后续的脚本实例就能检测到第一个脚本正在运行,并且能够与它进行交流。

如果你需要跨平台的功能,使用套接字作为通信通道是个不错的选择。你可以指定一个“知名端口”,比如12345,专门留给你的脚本使用,并在这个端口上打开一个只监听本地的套接字(127.0.0.1)。如果打开这个套接字失败了,说明这个端口已经被占用,那么你就可以连接到这个端口,这样就能和已经在运行的脚本进行通信。

如果你对套接字编程不太了解,可以参考一个不错的 HOWTO 文档,在这里可以找到。你也可以看看 《Python编程快速上手》 这本书的相关章节(当然,我对这本书有点偏爱;-)。

11

Alex Martelli的方法是建立一个通信通道,这个方法是合适的。我会使用multiprocessing.connection.Listener来创建一个监听器,你可以根据自己的需要选择。相关文档可以在这里找到:http://docs.python.org/library/multiprocessing.html#multiprocessing-listeners-clients

你可以选择使用AF_UNIX(适用于Linux)或者AF_PIPE(适用于Windows),而不是使用AF_INET(套接字)。希望加个小“如果”不会有什么问题。

编辑:我想给个例子也不错。虽然这个例子很基础。

#!/usr/bin/env python

from multiprocessing.connection import Listener, Client
import socket
from array import array
from sys import argv

def myloop(address):
    try:
        listener = Listener(*address)
        conn = listener.accept()
        serve(conn)
    except socket.error, e:
        conn = Client(*address)
        conn.send('this is a client')
        conn.send('close')

def serve(conn):
    while True:
        msg = conn.recv()
        if msg.upper() == 'CLOSE':
            break
        print msg
    conn.close()

if __name__ == '__main__':
    address = ('/tmp/testipc', 'AF_UNIX')
    myloop(address)

这个在OS X上可以运行,所以需要在Linux和(替换成正确的地址后)Windows上测试。从安全的角度来看,有很多需要注意的地方,最主要的是conn.recv会解包它的数据,所以你通常使用recv_bytes会更好。

撰写回答