【问题标题】:Is using a Mutex to prevent multiple instances of the same program from running safe?是否使用互斥锁来防止同一程序的多个实例安全运行?
【发布时间】:2025-12-08 16:40:01
【问题描述】:

我正在使用此代码来防止我的程序的第二个实例同时运行,它安全吗?

Mutex appSingleton = new System.Threading.Mutex(false, "MyAppSingleInstnceMutx");
if (appSingleton.WaitOne(0, false)) {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new MainForm());
    appSingleton.Close();
} else {
    MessageBox.Show("Sorry, only one instance of MyApp is allowed.");
}

我担心如果某些东西引发异常并且应用程序崩溃,Mutex 仍将被保留。这是真的吗?

【问题讨论】:

标签: c# winforms mutex single-instance


【解决方案1】:

为此目的使用 Windows 事件更为常见和方便。例如

static EventWaitHandle s_event ;

bool created ;
s_event = new EventWaitHandle (false, 
    EventResetMode.ManualReset, "my program#startup", out created) ;
if (created) Launch () ;
else         Exit   () ;

当您的进程退出或终止时,Windows 将为您关闭该事件,如果没有打开的句柄,则将其销毁。

添加:要管理会话,请使用 Local\Global\ 前缀作为事件(或互斥体)名称。如果您的应用程序是每个用户的,只需将一个经过适当修改的登录用户名附加到事件名称。

【讨论】:

  • 对。这比使用互斥锁更清洁、更安全,因为它避免了在互斥锁上同步的需要,并尝试处理所有类型的竞争条件......请注意,它甚至没有必要(或有用)等待事件。本质上,您只需检查它是否存在。
  • 您可以以同样的方式使用互斥锁。也就是说,只需使用 createdNew 标志来确定您的标志是否是第一个实例。无需在互斥锁上同步。所以 EventWaitHandle 方法并不比 Mutex 方法好或差。你也可以使用 Semaphore。
  • 这比 99% 的解决方案简单得多,但这是我唯一一次看到它。谢谢!
  • 可以用更简单的方式实现这一点:使用 (new EventWaitHandle(false, EventResetMode.ManualReset, "my program#startup", out var created)) { if (created) { Launch(); } }
  • 虽然更方便,甚至可能更安全、更清洁,但这种方法不是跨平台兼容的。使用 .NET Core 开发应用程序时,此代码将引发异常。 (Source)
【解决方案2】:

通常是的,这会起作用。然而,魔鬼在细节中。

首先,您要关闭 finally 块中的互斥锁。否则,您的进程可能会突然终止并使其处于信号状态,例如异常。这将使未来的流程实例无法启动。

不幸的是,即使使用finally 块,您也必须处理进程将在不释放互斥锁的情况下终止的可能性。例如,如果用户通过 TaskManager 终止进程,就会发生这种情况。您的代码中有一个竞争条件,允许第二个进程在WaitOne 调用中获得AbandonedMutexException。您需要为此制定恢复策略。

我鼓励您阅读the details of the Mutex class。使用它并不总是那么简单。


扩展竞争条件的可能性:

可能会发生以下一系列事件,这些事件会导致应用程序的第二个实例抛出:

  1. 正常进程启动。
  2. 第二个进程启动并获取互斥锁的句柄,但在WaitOne 调用之前被关闭。
  3. 进程 #1 突然终止。互斥体没有被破坏,因为进程#2 有一个句柄。而是将其设置为废弃状态。
  4. 第二个进程再次开始运行并获得AbanonedMutexException

【讨论】:

  • 如果他的代码是唯一创建互斥锁的代码,在进程终止时会销毁,因此create + waitOne会成功。
  • @Michael,如果互斥锁没有明确释放,它将被置于废弃状态
  • @Michael:在我的答案中试试我的代码,看看。进程退出,互斥体被销毁。重新运行 exe 时会重新创建它。
  • @Michael 您忽略了竞争条件的可能性。有可能启动第二个进程,对互斥体进行分级,第一个进程死亡,抛出废弃互斥体异常。
  • @JaredPar:是的,如果原始进程在创建互斥锁的代码和 WaitOne 之间抛出异常,这可能会发生。我的 cmets 仅与引发异常情况的通用进程有关。
【解决方案3】:

您可以使用互斥体,但首先要确保这确实是您想要的。

因为“避免多个实例”没有明确定义。这可能意味着

  1. 避免在同一个用户会话中启动多个实例,无论该用户会话有多少个桌面,但允许多个实例针对不同的用户会话同时运行。
  2. 避免在同一个桌面上启动多个实例,但允许多个实例运行,只要每个实例都在单独的桌面上。
  3. 避免为同一个用户帐户启动多个实例,无论存在多少在此帐户下运行的桌面或会话,但允许多个实例同时运行在不同用户帐户下运行的会话。
  4. 避免在同一台机器上启动多个实例。这意味着,无论任意数量的用户使用多少个桌面,最多只能运行一个程序实例。

通过使用互斥体,您基本上使用的是定义数字 4。

【讨论】:

  • 我想提一下,通过使用命名互斥锁,通常使用定义编号 1,因为默认情况下互斥锁将驻留在会话命名空间中。
【解决方案4】:

我使用这种方法,我认为它是安全的,因为如果不再由任何应用程序持有 Mutex,则 Mutex 将被销毁(如果应用程序最初无法创建 Mutext,则应用程序将被终止)。这在“AppDomain-processes”中可能会或可能不会同样有效(参见底部的链接):

// Make sure that appMutex has the lifetime of the code to guard --
// you must keep it from being collected (and the finalizer called, which
// will release the mutex, which is not good here!).
// You can also poke the mutex later.
Mutex appMutex;

// In some startup/initialization code
bool createdNew;
appMutex = new Mutex(true, "mutexname", out createdNew);
if (!createdNew) {
  // The mutex already existed - exit application.
  // Windows will release the resources for the process and the
  // mutex will go away when no process has it open.
  // Processes are much more cleaned-up after than threads :)
} else {
  // win \o/
}

上述内容受到其他答案/cmets 中关于恶意程序能够位于互斥体上的注释的影响。这里不用担心。此外,在“本地”空间中创建的无前缀互斥锁。这可能是正确的。

见:http://ayende.com/Blog/archive/2008/02/28/The-mysterious-life-of-mutexes.aspx -- Jon Skeet 附带 ;-)

【讨论】:

    【解决方案5】:

    在 Windows 上,终止进程会产生以下结果:

    • 进程中的所有剩余线程都标记为终止。
    • 进程分配的所有资源都会被释放。
    • 所有内核对象都已关闭。
    • 进程代码已从内存中删除。
    • 进程退出代码已设置。
    • 进程对象发出信号。

    Mutex 对象是内核对象,因此进程所持有的任何对象都会在进程终止时关闭(无论如何在 Windows 中)。

    但是,请注意 CreateMutex() 文档中的以下内容:

    如果您使用命名互斥体将应用程序限制为单个实例,则恶意用户可以在您创建此互斥体之前阻止您的应用程序启动。

    【讨论】:

    • Mutex 是一个共享​​>资源,只有当所有持有该资源的进程都停止时才会被销毁。他的代码中有一个竞争条件,允许在 WaitOne 调用时抛出 AbandonedMutexException。
    • 我很抱歉——我的回答是针对本机代码的——我应该更仔细地阅读。但是,当线程死亡时,CLR 将释放线程持有的 Mutex。当进程终止时,Windows 将关闭任何持有的互斥体(这将释放它们)。关闭互斥锁可能不会破坏它。
    【解决方案6】:

    是的,这很安全,我建议使用以下模式,因为您需要确保始终释放 Mutex

    using( Mutex mutex = new Mutex( false, "mutex name" ) )
    {
        if( !mutex.WaitOne( 0, true ) )
        {
            MessageBox.Show("Unable to run multiple instances of this program.",
                            "Error",  
                            MessageBoxButtons.OK, 
                            MessageBoxIcon.Error);
        }
        else
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());                  
        }
    }
    

    【讨论】:

    • 至少在 Windows 上(也可能在 Linux 上),进程终止会自动释放所有持有的同步对象。
    【解决方案7】:

    这里是代码sn-p

    public enum ApplicationSingleInstanceMode
    {
        CurrentUserSession,
        AllSessionsOfCurrentUser,
        Pc
    }
    
    public class ApplicationSingleInstancePerUser: IDisposable
    {
        private readonly EventWaitHandle _event;
    
        /// <summary>
        /// Shows if the current instance of ghost is the first
        /// </summary>
        public bool FirstInstance { get; private set; }
    
        /// <summary>
        /// Initializes 
        /// </summary>
        /// <param name="applicationName">The application name</param>
        /// <param name="mode">The single mode</param>
        public ApplicationSingleInstancePerUser(string applicationName, ApplicationSingleInstanceMode mode = ApplicationSingleInstanceMode.CurrentUserSession)
        {
            string name;
            if (mode == ApplicationSingleInstanceMode.CurrentUserSession)
                name = $"Local\\{applicationName}";
            else if (mode == ApplicationSingleInstanceMode.AllSessionsOfCurrentUser)
                name = $"Global\\{applicationName}{Environment.UserDomainName}";
            else
                name = $"Global\\{applicationName}";
    
            try
            {
                bool created;
                _event = new EventWaitHandle(false, EventResetMode.ManualReset, name, out created);
                FirstInstance = created;
            }
            catch
            {
            }
        }
    
        public void Dispose()
        {
            _event.Dispose();
        }
    }
    

    【讨论】:

      【解决方案8】:

      这是我的处理方式

      在程序类中:

      1. 使用 Process.GetCurrentProcess() 获取您的应用程序的 System.Diagnostics.Process

      2. 使用 Process.GetProcessesByName(thisProcess.ProcessName) 以您的应用程序的当前名称逐步浏览打开的进程集合

      3. 对照thisProcess.Id检查每个process.Id,如果已经打开了一个实例,那么至少有1个匹配名称但不匹配Id,否则继续打开实例

        using System.Diagnostics;
        
        .....    
        
        static void Main()
        {
           Process thisProcess = Process.GetCurrentProcess();
           foreach(Process p in Process.GetProcessesByName(thisProcess.ProcessName))
           {
              if(p.Id != thisProcess.Id)
              {
                 // Do whatever u want here to alert user to multiple instance
                 return;
              }
           }
           // Continue on with opening application
        

      完成此操作的一个很好的方法是将已经打开的实例呈现给用户,他们可能不知道它是打开的,所以让我们向他们展示它。为此,我使用 User32.dll 将一条消息广播到 Windows 消息传递循环中,一条自定义消息,我让我的应用程序在 WndProc 方法中侦听它,如果它收到此消息,它会将自己呈现给用户,Form .Show() 之类的。

      【讨论】:

        【解决方案9】:

        如果您想使用基于互斥锁的方法,您应该真正使用本地互斥锁到restrict the approach to just the curent user's login session。还要注意该链接中关于使用互斥锁方法进行稳健资源处理的另一个重要警告。

        需要注意的是,基于互斥锁的方法不允许您在用户尝试启动第二个实例时激活应用的第一个实例。

        另一种方法是 PInvoke 到 FindWindow,然后在第一个实例上使用 SetForegroundWindow。另一种选择是按名称检查您的进程:

        Process[] processes = Process.GetProcessesByName("MyApp");
        if (processes.Length != 1)
        {
            return;
        } 
        

        后一种选择都有一个假设的竞争条件,即应用程序的两个实例可以同时启动,然后相互检测。这在实践中不太可能发生 - 事实上,在测试期间我无法实现。

        后两种替代方案的另一个问题是它们在使用终端服务时不起作用。

        【讨论】:

        • 检查进程表会遇到竞争条件。如果同时启动两个实例,则两者都将报告预先存在的应用程序。此外,要击败它,您所要做的就是制作可执行文件的副本。 FindWindow 遭受相同的竞争条件。
        • 我更改了答案以回复您的 cmets。至于通过制作可执行文件的副本来“击败”检查。 OP 几乎可以肯定不是在询问是否故意企图挫败检查。
        • 另一方面,我在使用 FindWindow 时遇到了竞争条件。我从未使用过进程表方法,但我发现的一件事是,如果可能发生某些事情,它最终会发生。通常在最不合时宜的时候。
        【解决方案10】:

        使用具有超时和安全设置的应用程序,避免 AbandonedMutexException。我使用了我的自定义类:

        private class SingleAppMutexControl : IDisposable
            {
                private readonly Mutex _mutex;
                private readonly bool _hasHandle;
        
                public SingleAppMutexControl(string appGuid, int waitmillisecondsTimeout = 5000)
                {
                    bool createdNew;
                    var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null),
                        MutexRights.FullControl, AccessControlType.Allow);
                    var securitySettings = new MutexSecurity();
                    securitySettings.AddAccessRule(allowEveryoneRule);
                    _mutex = new Mutex(false, "Global\\" + appGuid, out createdNew, securitySettings);
                    _hasHandle = false;
                    try
                    {
                        _hasHandle = _mutex.WaitOne(waitmillisecondsTimeout, false);
                        if (_hasHandle == false)
                            throw new System.TimeoutException();
                    }
                    catch (AbandonedMutexException)
                    {
                        _hasHandle = true;
                    }
                }
        
                public void Dispose()
                {
                    if (_mutex != null)
                    {
                        if (_hasHandle)
                            _mutex.ReleaseMutex();
                        _mutex.Dispose();
                    }
                }
            }
        

        并使用它:

            private static void Main(string[] args)
            {
                try
                {
                    const string appguid = "{xxxxxxxx-xxxxxxxx}";
                    using (new SingleAppMutexControl(appguid))
                    {
                        //run main app
                        Console.ReadLine();
                    }
                }
                catch (System.TimeoutException)
                {
                    Log.Warn("Application already runned");
                }
                catch (Exception ex)
                {
                    Log.Fatal(ex, "Fatal Error on running");
                }
            }
        

        【讨论】:

          【解决方案11】:

          请务必记住,可以通过 Microsoft sysinternals 中的 handle.exe 等程序轻松查看此互斥体\事件,然后如果使用常量名称,邪恶的人可能会使用它来阻止您的应用启动。

          以下是 Microsoft 关于安全替代品的建议中的一些引述:

          但是,恶意用户可以先于您创建此互斥体,并且 阻止您的应用程序启动。为了防止这种情况, 创建一个随机命名的互斥体并存储名称,以便它只能 由授权用户获取。或者,您可以使用文件 以此目的。要将您的应用程序限制为每个用户一个实例, 在用户的配置文件目录中创建一个锁定文件。

          取自这里: https://docs.microsoft.com/en-us/sysinternals/downloads/handle

          【讨论】: