实现进程单例运行(go语言版)

  1. 单例进程的理解
    有些程序在设备上只允许运行1个,机运行第二个时会自动判断已有进程是否运行,如果有则直接退出。那么这些进程是如何做到的?答案有很多,下面列举一下我看到过的方法
1.linux下使用 ps -ef|grep 进程名,Windows下使用tasklist | findstr 进程名,这样可以找到是否已有进程在运行
  这种方案无疑是最简单的,但是存在一个问题,程序如果改名了怎么办?
2.进程运行时创建一个文件 tmp,当进程退出后删除该文件,如果第二次运行该进程则检测tmp文件是否存在
  这种方案也有一个弊端,及进程异常退出tmp文件没有被删除咋办?
3.另外就是为文件加锁,进程运行时一直锁定文件,退出或者异常退出则释放文件锁
  这种方案无疑是最可靠的,也是很多项目正在使用的方案,具体实现看下面
4.进程启动连接localhost:xxxx端口,如果有进程监听该端口则说明已有进程运行,没有则本次运行监听
  该方案最省事,但是必须占用本机一个端口,程序退出或异常退出时会取消监听端口
  1. 下面介绍go语言的文件锁实现单例运行
package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "syscall"
)

/**
 1. 文件已被某个进程锁住则说明已有进程运行
 2. 如果没有进程锁定则写入当前进程pid到文件
 3. pid文件如果不存在则会主动创建一个
 4. 返回:true => 已有进程运行,false => 没有进程运行
**/
func SingleProcess(sigfile string) bool {
    // 打开一个文件,如果文件不存在则创建
    f, err := os.OpenFile(sigfile, os.O_RDONLY|os.O_CREATE, os.ModePerm)
    if err != nil {
        return true
    }

    // 下面是为文件加锁
    if syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB) != nil {
        return true
    }

    // 锁住的文件关闭会释放锁
    //if err = f.Close(); err != nil {
    //    return err
    //}

    // 下面是写入当前进程的pid到文件
    if ioutil.WriteFile(sigfile, []byte(fmt.Sprintln(os.Getpid())), os.ModePerm) != nil {
        return true
    }
    return false
}
  1. 下面介绍监听本地端口方式实现单例进程
/**
 5.  当尝试连接失败时,会启动协程一直监听端口
 6.  这样再次运行该进程时,就能成功连接该端口
 7.  返回true -> 已有进程运行,false -> 没有进程运行
**/
func SingleProcess(port int) bool {
    // 得到本地监听地址和端口
    address := fmt.Sprintf("localhost:%d", port)

    // 尝试连接服务器
    conn, err := net.Dial("tcp", address)
    if err != nil {
        go func() { /* 连接不成功,启用服务 */
            l, err := net.Listen("tcp", address)
            if err != nil {
                return
            }
            defer l.Close()

            for {
                l.Accept()
            }
        }()
        // 连接失败,说明没有进程运行
        return false
    }
    defer conn.Close()

    // 连接成功说明已有进程监听
    return true
}
  1. 以上就是我的两种方案,个人比较倾向于文件锁,但是这种方法仅适用于linux,在Windows下还是老老实实用占用端口的方案吧。

captcha