用 Python 装饰器写一个single instance

一、关于python的装饰器

python装饰器的形式有点像java的注解、用处有点像java的切面。可以简单的理解为一个函数如果被装饰器注解了,那么在运行该函数之前,会先调用装饰器函数。

具体的参考如下两篇文章:

https://www.zhihu.com/question/26930016

http://www.cnblogs.com/rhcad/archive/2011/12/21/2295507.html

二、一个脚本单一实例运行

有时候我们会有这种需求,只允许某个程序在系统中只有一个运行实例,即不允许程序“多开”。

There are several common techniques including using semaphores. The one I see used most often is to create a “pid lock file” on startup that contains the pid of the running process. If the file already exists when the program starts up, open it up and grab the pid inside, check to see if a process with that pid is running, if it is check the cmdline value in /proc/pid to see if it is an instance of your program, if it is then quit, otherwise overwrite the file with your pid. The usual name for the pid file is application_name.pid.

上面这段话抄袭自stackoverflow上的回答。就是写一个application_name.pid文件(该文件通常放在/var/run目录下),里面放着当前运行实例的pid。程序运行前先检查该pid的进程是否存在,存在则说明已经启动了该程序了,当前程序自动退出;如果不存在,则说明上次程序已经终止,可以运行当前程序,同时修改该pid值。

另外,除了使用pid文件的方法,还可以使用其他互斥方式:比如说绑定某个端口号,因为一个端口号只能被一个进程绑定。

三、写一个限制脚本单实例运行的装饰器

3.1 使用pid文件的方式

# -*- coding: utf-8 -*-
import logging
import time
import os
import fcntl
from functools import wraps


def single_instance_by_pid_file(pid_file):
    def _wrapper(func):
        @wraps(func)
        def __wrapper(*args, **kwargs):
            pid = str(os.getpid())
            fp = open(pid_file, 'a+')
            try:
                # LOCK_EX表示对fp文件加互斥锁;LOCK_NB表示如果无法加锁,直接抛出IOError异常而不是阻塞等待
                fcntl.flock(fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
                logging.debug("锁定pid文件[%s]", pid_file)
                pass
            except IOError, e:
                logging.error("已有进程占用pid文件[%s]", pid_file)
                return None

            fp.seek(0)
            fp.truncate()
            fp.write(pid)
            fp.flush()

            try:
                result = func(*args, **kwargs)
                pass
            except Exception, e:
                raise e
            else:
                return result
            finally:
                fp.close()
                pass

        return __wrapper

    return _wrapper


@single_instance_by_pid_file("/tmp/testSingle.pid")
def main():
    """装饰器测试主函数"""
    logging.info("main process start")
    time.sleep(20)
    logging.info("main process end")
    pass


if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(threadName)s] %(levelname)5s: %(message)s")
    main()

ps:上述代码中的fcntl只适用在linux、mac系统,windows不适用 =。=#

3.2 使用端口绑定的方式

# -*- coding: utf-8 -*-
import logging
import time
import socket
from functools import wraps


def single_instance_by_port(port):
    def _wrapper(func):
        @wraps(func)
        def __wrapper(*args, **kwargs):
            try:
                # 定义s为全局变量,否则变量会在方法退出后被销毁
                global s
                s = socket.socket()
                host = socket.gethostname()
                s.bind((host, port))
                logging.info("绑定端口号[%s]", port)
                pass
            except Exception, e:
                logging.warn("端口号[%s]已被占用", port)
                logging.exception(e)
                return None
            return func(*args, **kwargs)

        return __wrapper

    return _wrapper


@single_instance_by_port(6666)
def main():
    """装饰器测试主函数"""
    logging.info("main process start")
    time.sleep(10)
    logging.info("main process end")
    pass


if __name__ == "__main__":
    logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(threadName)s] %(levelname)5s: %(message)s")
    main()

 

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top