如何确定之前的应用程序实例是否正在运行?

27

我有一个用C#编写的控制台应用程序,其中运行各种奥妙的自动化任务。我非常清楚这实际上应该是一个Windows服务,因为它需要持续运行,但我目前不想这样做。(所以,请不要把它作为一个答案建议)。

与此同时,我需要一些示例C#代码,以便让我确定应用程序是否已经在运行中。

在旧的VB6.0时代,我会使用App.PrevInstance()

我希望能在我的Main方法中做到这一点:

static void Main()
{
  if(!MyApp.IsAlreadyRunning())
  {
    while(true)
    {
      RockAndRollAllNightAndPartyEveryDay();
    }
  }
}
10个回答

28

使用互斥锁的正确方法:

private static Mutex mutex;

static void Main()
{
    // STEP 1: Create and/or check mutex existence in a race-free way
    bool created;
    mutex = new Mutex(false, "YourAppName-{add-your-random-chars}", out created);
    if (!created)
    {
        MessageBox.Show("Another instance of this application is already running");
        return;
    }

    // STEP 2: Run whatever the app needs to do
    Application.Run(new Form1());

    // No need to release the mutex because it was never acquired
}

以上方法无法检测同一台机器上不同用户账户下运行该应用程序的情况。类似的情况还有进程可以同时在服务主机和独立模式下运行。为了解决这些问题,请按照以下方式创建互斥对象:

        var sid = new SecurityIdentifier(WellKnownSidType.WorldSid, null);
        var mutexsecurity = new MutexSecurity();
        mutexsecurity.AddAccessRule(new MutexAccessRule(sid, MutexRights.FullControl, AccessControlType.Allow));
        mutexsecurity.AddAccessRule(new MutexAccessRule(sid, MutexRights.ChangePermissions, AccessControlType.Deny));
        mutexsecurity.AddAccessRule(new MutexAccessRule(sid, MutexRights.Delete, AccessControlType.Deny));
        _mutex = new Mutex(false, "Global\\YourAppName-{add-your-random-chars}", out created, mutexsecurity);

这里有两个不同之处 - 首先,互斥对象需要使用安全权限创建,以允许其他用户帐户打开/获取它。其次,在服务主机下运行的服务情况下,名称必须以"Global"为前缀(不确定其他用户在同一台计算机上运行的情况)。


从原始问题中并不清楚“previous instance”是指本次会话、对于该用户还是在这台机器上。 互斥体名称默认为每个会话,因此每台机器的解决方案应使用类似于@"Global\YourAppName-{add-your-random-chars}"的名称。 http://msdn.microsoft.com/en-us/library/aa382954%28VS.85%29.aspx - adrianm
这是我的问题,讨论仅检查互斥锁的存在而不获取它是否合适。 - Roman Starkov
@romkyns:你能解释一下为什么创建“Global\”互斥体的结果取决于应用程序运行的会话吗?场景:
  1. 用户A调用new Mutex(true,“Global\xxx”,out created)。返回时,created为TRUE
  2. 同一会话中的用户B调用new Mutex(true,“Global\xxx”,out created)。调用成功,返回时created为FALSE
  3. 不同会话(切换用户)中的用户B调用new Mutex(true,“Global\xxx”,out created)。结果是一个异常:“拒绝访问路径“Global\xxx””
为什么(2)的结果与(3)不同?
- mistika
@mistika 我猜这是为了让你能够创建其他用户无法获取的互斥锁。如果你不完全信任这些用户,这可能会很有用。 - Roman Starkov
@romkyns:还是不明白。假设用户A不信任用户B,为什么如果用户B在与用户A相同的会话中调用CreateMutex时不会收到AccessDenied呢?我看不出来为什么当用户B的进程在与用户A相同的会话中运行时,用户A应该信任用户B的进程的原因。 - mistika
@mistika,我现在明白你的意思了。我不知道答案,建议你在这里发布一个问题。 - Roman Starkov

16

Jeroen已经回答了这个问题,但是到目前为止最好的方法是使用Mutex...而不是Process。以下是更详细的答案和代码。

在看到一些关于竞赛条件的评论后,我更新了这个答案,通过使用Mutex构造函数来解决这个问题。

Boolean createdNew;
Mutex mutex;

try
{      
   mutex = new Mutex(false, "SINGLEINSTANCE" out createdNew);
   if (createdNew == false)
   {
      Console.WriteLine("Error : Only 1 instance of this application can run at a time");
      Application.Exit();
   }

   // Run your application
}
catch (Exception e)
{
    // Unable to open the mutex for various reasons
}
finally 
{
    // If this instance created the mutex, ensure that
    // it's cleaned up, otherwise we can't restart the
    // application
    if (mutex && createdNew) 
    {
        mutex.ReleaseMutex();
        mutex.Dispose();
    }
}

请注意 try{} finally{} 块。如果您的应用程序崩溃或干净退出,但未释放 Mutex,则可能无法稍后重新启动它。


我的理解是,在基于NT的操作系统中,无论何种原因导致进程终止,该进程创建的所有内核对象都会被操作系统释放。这正确吗? - Jim In Texas
4
获取互斥锁存在竞态条件;请参考我的备选答案。同时,提及“SINGLEINSTANCE”应该对应于应用程序是必须的! - Roman Starkov
1
@Ian - 我建议的内容太长了,无法在评论中发布,这就是为什么我发布了一个答案。当然,你不能跨进程使用“lock”,但是你可以一次性获取互斥锁并检查其存在性,而不会出现竞态条件。 - Roman Starkov
1
-1 这不应该是被接受的答案,无论它是在书中还是其他地方。这可能会导致危险的竞态条件,其中多个应用程序实例最终成功打开相同的 Mutex,这违背了全局 Mutex 的整个目的。 - caesay
@caesay,我已经更新了这个答案,试图消除竞态条件 - 因为我不想鼓励糟糕的代码。 - Ian
显示剩余2条评论

4

最简单(也是最可靠)的方法是使用 Mutex。使用 Mutex 类的 WaitOne 方法等待 Mutex 可用。另一个好处是,这不需要任何无限循环。


2
    // Allow running single instance
    string processName = Process.GetCurrentProcess().ProcessName;
    Process[] instances = Process.GetProcessesByName(processName);

    if (instances.Length > 1)
    {
        MessageBox.Show("Application already Running", "Error 1001 - Application Running");
        return;
    }

如果应用程序已经在运行,则使用上面显示的消息框优雅地退出应用程序。


2
您可以搜索现有系统进程的进程名称。例如,请参阅此博客文章以获取代码示例。
您还可以使用命名系统互斥体来查看您的进程是否已在运行。这里有一些示例代码。在我的经验中,这种方法更加可靠,并且是更简单、易懂的代码。

2
这篇文章讲述了如何防止多个进程实例运行:Prevent a second process instance from running。它是用VB.net编写的,但你可以进行转换。
撰写一个通用函数来检查当前应用程序是否已经在运行中的问题在于Process对象的ProcessName属性似乎仅限于15个字符,因此更长的进程名称会被截断。
获取进程名称的更安全的方法是获取其主模块的文件名并删除扩展名。以下可重复使用的程序即采用了这种方法。

Function AppIsAlreadyRunning() As Boolean
    ' get the filename of the main module
    Dim moduleName As String = Process.GetCurrentProcess.MainModule.ModuleName

    ' discard the extension to get the process name
    Dim procName As String = System.IO.Path.GetFileNameWithoutExtension(moduleName)

    ' return true if there are 2 or more processes with that name
    If Process.GetProcessesByName(procName).Length > 1 Then
        Return True
    End If
End Function

1
你可以在 System.Diagnostics 命名空间中使用 Process.GetProcessesByName("MyProcessName"); 来检查是否有你的进程实例正在运行。
编辑:评论中有非常好的观察!这是一种(非常)简单的方法,肯定不能涵盖所有情况。

从我评论中的文章中...编写一个通用函数来检查当前应用程序是否已经在运行中的问题在于Process对象的ProcessName属性似乎被限制为15个字符,因此更长的进程名称会被截断。例如,创建默认的ConsoleApplication1应用程序并运行此代码: - Ron
这也会返回当前进程,因此请确保从此列表中过滤掉您当前进程的ID(或检查返回的数组长度是否>1,而不是>0)。 - Reed Copsey
1
如果应用程序存在两个副本并且其中一个已被重命名,则此方法无法正常工作。 - Murray

1
使用核对象是在Windows中实现单个实例保护的唯一正确方式。
如果其他人从Stackoverflow复制了这行代码并在你之前运行了他们的程序,那么这个语句:mutex = Mutex.OpenExisting("SINGLEINSTANCE"); 就不起作用了,因为那个人先占用了"SINGLEINSTANCE"。所以你需要在mutex名称中包含一个GUID:
mutex = Mutex.OpenExisting("MyApp{AD52DAF0-C3CF-4cc7-9EDD-03812F82557E}");
这种技术将防止当前用户运行多个实例,但不能防止其他用户这样做。
为确保本地计算机上只能运行应用程序的一个实例,你需要这样做:
mutex = Mutex.OpenExisting("Global\MyApp{AD52DAF0-C3CF-4cc7-9EDD-03812F82557E}");
请参阅CreateMutex api的帮助。

0

-3

另一种方法是绑定到本地机器上的地址(就像TCP监听器一样)。每次只能有一个进程绑定到一个端口/地址组合。因此,在环回适配器上选择一个端口并开始使用它。

这有以下好处:

  • 即使有人重命名可执行文件,也可以正常工作
  • 应用程序崩溃时会自动重置
  • 该技术在其他操作系统上也是可移植的

不足之处在于,如果有另一个应用程序绑定到该特定端口,则可能会失败。


根据要求,下面是一些绑定到地址/端口的代码。这是从其他地方摘出来的。它不完整,但必要的部分都在这里。

using System.Net;
using System.Net.Sockets;

[...]

// Make up a number that's currently unused by you, or any 
// well-known service. i.e. 80 for http, 22 for ssh, etc..
int portNum = 2001;  

// This binds to any (meaning all) adapters on this system 
IPAddress ipAddress = IPAddress.Any;
IPEndPoint localEndPoint = new IPEndPoint(ipAddress, portNum);
Socket listener = new Socket(AddressFamily.InterNetwork,
    SocketType.Stream, ProtocolType.Tcp );

// The next statement will throw an exception if anyone has done this Bind!
listener.Bind(localEndPoint);

只要监听器没有被垃圾回收(超出范围)或程序没有终止:那个适配器上的端口就是你的,只属于你。如果任何事情发生在监听器上,那么它就变得可供其他人使用。为了锁定的目的,你应该在某个地方将监听器设为静态。

你有关于如何执行此操作的示例代码吗?另外,“当应用程序崩溃时重置自身”是什么意思? - Jose Basilio
3
这种技术相当脆弱,使用互斥锁技术会更好。 - Jim In Texas
2
如果我在我的机器上安装的应用程序由于我已经配置了VNC监听该端口(出于各种原因),而无法运行,那将非常令人恼火。我怀疑这也可能会触发防火墙提示。请改用互斥量方法。 - Roman Starkov

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