【问题标题】:What is a good pattern for using a Global Mutex in C#?在 C# 中使用全局互斥锁的好模式是什么?
【发布时间】:2010-09-18 18:39:49
【问题描述】:

Mutex 类很容易被误解,全局互斥体更是如此。

在创建全局互斥体时使用什么好的、安全的模式?

一个可以工作的

  • 无论我的机器所在的语言环境如何
  • 保证正确释放互斥锁
  • 如果未获取互斥锁,则可选地不会永远挂起
  • 处理其他进程放弃互斥锁的情况

【问题讨论】:

    标签: c# concurrency mutex


    【解决方案1】:

    没有 WaitOne 的解决方案(针对 WPF),因为它可能导致 AbandonedMutexException。 此解决方案使用返回 createdNew 布尔值的 Mutex 构造函数来检查互斥体是否已创建。它还使用 GetType().GUID,因此重命名可执行文件不允许多个实例。

    全局与本地互斥锁见注释: https://docs.microsoft.com/en-us/dotnet/api/system.threading.mutex?view=netframework-4.8

    private Mutex mutex;
    private bool mutexCreated;
    
    public App()
    {
        string mutexId = $"Global\\{GetType().GUID}";
        mutex = new Mutex(true, mutexId, out mutexCreated);
    }
    
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        if (!mutexCreated)
        {
            MessageBox.Show("Already started!");
            Shutdown();
        }
    }
    

    因为 Mutex 实现了 IDisposable,所以它会自动释放,但为了完整性调用 dispose:

    protected override void OnExit(ExitEventArgs e)
    {
        base.OnExit(e);
        mutex.Dispose();
    }
    

    将所有内容移到基类中,并从接受的答案中添加 allowEveryoneRule。还添加了 ReleaseMutex,虽然它看起来并不真正需要,因为它是由操作系统自动释放的(如果应用程序崩溃并且从不调用 ReleaseMutex,你需要重新启动吗?)。

    public class SingleApplication : Application
    {
        private Mutex mutex;
        private bool mutexCreated;
    
        public SingleApplication()
        {
            string mutexId = $"Global\\{GetType().GUID}";
    
            MutexAccessRule allowEveryoneRule = new MutexAccessRule(
                new SecurityIdentifier(WellKnownSidType.WorldSid, null),
                MutexRights.FullControl, 
                AccessControlType.Allow);
            MutexSecurity securitySettings = new MutexSecurity();
            securitySettings.AddAccessRule(allowEveryoneRule);
    
            // initiallyOwned: true == false + mutex.WaitOne()
            mutex = new Mutex(initiallyOwned: true, mutexId, out mutexCreated, securitySettings);        
        }
    
        protected override void OnExit(ExitEventArgs e)
        {
            base.OnExit(e);
            if (mutexCreated)
            {
                try
                {
                    mutex.ReleaseMutex();
                }
                catch (ApplicationException ex)
                {
                    MessageBox.Show(ex.Message, ex.GetType().FullName, MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
            mutex.Dispose();
        }
    
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            if (!mutexCreated)
            {
                MessageBox.Show("Already started!");
                Shutdown();
            }
        }
    }
    

    【讨论】:

      【解决方案2】:

      全局互斥锁不仅是为了确保应用程序只有一个实例。我个人更喜欢使用 Microsoft.VisualBasic 来确保像What is the correct way to create a single-instance WPF application?(Dale Ragan 回答)中所述的单实例应用程序...我发现将新应用程序启动时收到的参数传递给初始单实例应用程序更容易。

      但是关于这个线程中的一些以前的代码,我宁愿不要在每次想要锁定互斥锁时都创建互斥锁。对于单实例应用程序来说可能没问题,但在其他用途​​中,我觉得它有点矫枉过正。

      这就是为什么我建议改用这个实现:

      用法:

      static MutexGlobal _globalMutex = null;
      static MutexGlobal GlobalMutexAccessEMTP
      {
          get
          {
              if (_globalMutex == null)
              {
                  _globalMutex = new MutexGlobal();
              }
              return _globalMutex;
          }
      }
      
      using (GlobalMutexAccessEMTP.GetAwaiter())
      {
          ...
      }   
      

      Mutex 全局包装器:

      using System;
      using System.Reflection;
      using System.Runtime.InteropServices;
      using System.Security.AccessControl;
      using System.Security.Principal;
      using System.Threading;
      
      namespace HQ.Util.General.Threading
      {
          public class MutexGlobal : IDisposable
          {
              // ************************************************************************
              public string Name { get; private set; }
              internal Mutex Mutex { get; private set; }
              public int DefaultTimeOut { get; set; }
              public Func<int, bool> FuncTimeOutRetry { get; set; }
      
              // ************************************************************************
              public static MutexGlobal GetApplicationMutex(int defaultTimeOut = Timeout.Infinite)
              {
                  return new MutexGlobal(defaultTimeOut, ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value);
              }
      
              // ************************************************************************
              public MutexGlobal(int defaultTimeOut = Timeout.Infinite, string specificName = null)
              {
                  try
                  {
                      if (string.IsNullOrEmpty(specificName))
                      {
                          Name = Guid.NewGuid().ToString();
                      }
                      else
                      {
                          Name = specificName;
                      }
      
                      Name = string.Format("Global\\{{{0}}}", Name);
      
                      DefaultTimeOut = defaultTimeOut;
      
                      FuncTimeOutRetry = DefaultFuncTimeOutRetry;
      
                      var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
                      var securitySettings = new MutexSecurity();
                      securitySettings.AddAccessRule(allowEveryoneRule);
      
                      Mutex = new Mutex(false, Name, out bool createdNew, securitySettings);
      
                      if (Mutex == null)
                      {
                          throw new Exception($"Unable to create mutex: {Name}");
                      }
                  }
                  catch (Exception ex)
                  {
                      Log.Log.Instance.AddEntry(Log.LogType.LogException, $"Unable to create Mutex: {Name}", ex);
                      throw;
                  }
              }
      
              // ************************************************************************
              /// <summary>
              /// 
              /// </summary>
              /// <param name="timeOut"></param>
              /// <returns></returns>
              public MutexGlobalAwaiter GetAwaiter(int timeOut)
              {
                  return new MutexGlobalAwaiter(this, timeOut);
              }
      
              // ************************************************************************
              /// <summary>
              /// 
              /// </summary>
              /// <param name="timeOut"></param>
              /// <returns></returns>
              public MutexGlobalAwaiter GetAwaiter()
              {
                  return new MutexGlobalAwaiter(this, DefaultTimeOut);
              }
      
              // ************************************************************************
              /// <summary>
              /// This method could either throw any user specific exception or return 
              /// true to retry. Otherwise, retruning false will let the thread continue
              /// and you should verify the state of MutexGlobalAwaiter.HasTimedOut to 
              /// take proper action depending on timeout or not. 
              /// </summary>
              /// <param name="timeOutUsed"></param>
              /// <returns></returns>
              private bool DefaultFuncTimeOutRetry(int timeOutUsed)
              {
                  // throw new TimeoutException($"Mutex {Name} timed out {timeOutUsed}.");
      
                  Log.Log.Instance.AddEntry(Log.LogType.LogWarning, $"Mutex {Name} timeout: {timeOutUsed}.");
                  return true; // retry
              }
      
              // ************************************************************************
              public void Dispose()
              {
                  if (Mutex != null)
                  {
                      Mutex.ReleaseMutex();
                      Mutex.Close();
                  }
              }
      
              // ************************************************************************
      
          }
      }
      

      服务员

      using System;
      
      namespace HQ.Util.General.Threading
      {
          public class MutexGlobalAwaiter : IDisposable
          {
              MutexGlobal _mutexGlobal = null;
      
              public bool HasTimedOut { get; set; } = false;
      
              internal MutexGlobalAwaiter(MutexGlobal mutexEx, int timeOut)
              {
                  _mutexGlobal = mutexEx;
      
                  do
                  {
                      HasTimedOut = !_mutexGlobal.Mutex.WaitOne(timeOut, false);
                      if (! HasTimedOut) // Signal received
                      {
                          return;
                      }
                  } while (_mutexGlobal.FuncTimeOutRetry(timeOut));
              }
      
              #region IDisposable Support
              private bool disposedValue = false; // To detect redundant calls
      
              protected virtual void Dispose(bool disposing)
              {
                  if (!disposedValue)
                  {
                      if (disposing)
                      {
                          _mutexGlobal.Mutex.ReleaseMutex();
                      }
      
                      // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                      // TODO: set large fields to null.
      
                      disposedValue = true;
                  }
              }
              // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
              // ~MutexExAwaiter()
              // {
              //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
              //   Dispose(false);
              // }
      
              // This code added to correctly implement the disposable pattern.
              public void Dispose()
              {
                  // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
                  Dispose(true);
                  // TODO: uncomment the following line if the finalizer is overridden above.
                  // GC.SuppressFinalize(this);
              }
              #endregion
          }
      }
      

      【讨论】:

        【解决方案3】:

        我想确保它存在,因为很难做到正确:

        using System.Runtime.InteropServices;   //GuidAttribute
        using System.Reflection;                //Assembly
        using System.Threading;                 //Mutex
        using System.Security.AccessControl;    //MutexAccessRule
        using System.Security.Principal;        //SecurityIdentifier
        
        static void Main(string[] args)
        {
            // get application GUID as defined in AssemblyInfo.cs
            string appGuid =
                ((GuidAttribute)Assembly.GetExecutingAssembly().
                    GetCustomAttributes(typeof(GuidAttribute), false).
                        GetValue(0)).Value.ToString();
        
            // unique id for global mutex - Global prefix means it is global to the machine
            string mutexId = string.Format( "Global\\{{{0}}}", appGuid );
        
            // Need a place to store a return value in Mutex() constructor call
            bool createdNew;
        
            // edited by Jeremy Wiebe to add example of setting up security for multi-user usage
            // edited by 'Marc' to work also on localized systems (don't use just "Everyone") 
            var allowEveryoneRule =
                new MutexAccessRule( new SecurityIdentifier( WellKnownSidType.WorldSid
                                                           , null)
                                   , MutexRights.FullControl
                                   , AccessControlType.Allow
                                   );
            var securitySettings = new MutexSecurity();
            securitySettings.AddAccessRule(allowEveryoneRule);
        
           // edited by MasonGZhwiti to prevent race condition on security settings via VanNguyen
            using (var mutex = new Mutex(false, mutexId, out createdNew, securitySettings))
            {
                // edited by acidzombie24
                var hasHandle = false;
                try
                {
                    try
                    {
                        // note, you may want to time out here instead of waiting forever
                        // edited by acidzombie24
                        // mutex.WaitOne(Timeout.Infinite, false);
                        hasHandle = mutex.WaitOne(5000, false);
                        if (hasHandle == false)
                            throw new TimeoutException("Timeout waiting for exclusive access");
                    }
                    catch (AbandonedMutexException)
                    {
                        // Log the fact that the mutex was abandoned in another process,
                        // it will still get acquired
                        hasHandle = true;
                    }
        
                    // Perform your work here.
                }
                finally
                {
                    // edited by acidzombie24, added if statement
                    if(hasHandle)
                        mutex.ReleaseMutex();
                }
            }
        }
        

        【讨论】:

        • 您可能希望省略 using 以检查 createdNew 并在 finally 中添加 mutex.Dispose()。我现在无法清楚地解释它(我不知道原因),但是当mutex.WaitOnecreatedNew 变为false 之后返回true 时,我遇到了这种情况(我在当前AppDomain,然后加载一个新的AppDomain,并在其中执行相同的代码)。
        • 1. exitContext = false 是否在 mutex.WaitOne(5000, false) 中做任何事情? It looks like it could only cause an assert in CoreCLR, 2. 如果有人想知道,在Mutex 的构造函数中,initiallyOwnedfalse 的原因部分由this MSDN article 解释。
        • 提示:在 ASP.NET 中使用 Mutex 时要小心:“Mutex 类强制执行线程标识,因此只能由获取它的线程释放互斥量。相比之下,Semaphore 类不强制执行线程标识。”。一个 ASP.NET 请求可以由多个线程提供服务。
        • startupnextinstance 事件在 VB.NET 中是否安全?不在 C# 中docs.microsoft.com/es-es/dotnet/api/…
        • 在不使用 WaitOne 的情况下查看我的答案。 stackoverflow.com/a/59079638/4491768
        【解决方案4】:

        有时通过示例学习最有帮助。在三个不同的控制台窗口中运行此控制台应用程序。您将看到您首先运行的应用程序首先获取互斥锁,而其他两个正在等待轮到它们。然后在第一个应用程序中按回车键,您会看到应用程序 2 现在通过获取互斥锁继续运行,但应用程序 3 正在等待轮到它。在应用程序 2 中按 Enter 后,您会看到应用程序 3 继续。这说明了互斥锁的概念,它保护一段代码只能由一个线程(在本例中为进程)执行,例如写入文件。

        using System;
        using System.Threading;
        
        namespace MutexExample
        {
            class Program
            {
                static Mutex m = new Mutex(false, "myMutex");//create a new NAMED mutex, DO NOT OWN IT
                static void Main(string[] args)
                {
                    Console.WriteLine("Waiting to acquire Mutex");
                    m.WaitOne(); //ask to own the mutex, you'll be queued until it is released
                    Console.WriteLine("Mutex acquired.\nPress enter to release Mutex");
                    Console.ReadLine();
                    m.ReleaseMutex();//release the mutex so other processes can use it
                }
            }
        }
        

        【讨论】:

          【解决方案5】:

          使用已接受的答案,我创建了一个辅助类,因此您可以像使用 Lock 语句一样使用它。只是想我会分享。

          用途:

          using (new SingleGlobalInstance(1000)) //1000ms timeout on global lock
          {
              //Only 1 of these runs at a time
              RunSomeStuff();
          }
          

          还有助手类:

          class SingleGlobalInstance : IDisposable
          {
              //edit by user "jitbit" - renamed private fields to "_"
              public bool _hasHandle = false;
              Mutex _mutex;
          
              private void InitMutex()
              {
                  string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value;
                  string mutexId = string.Format("Global\\{{{0}}}", appGuid);
                  _mutex = new Mutex(false, mutexId);
          
                  var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
                  var securitySettings = new MutexSecurity();
                  securitySettings.AddAccessRule(allowEveryoneRule);
                  _mutex.SetAccessControl(securitySettings);
              }
          
              public SingleGlobalInstance(int timeOut)
              {
                  InitMutex();
                  try
                  {
                      if(timeOut < 0)
                          _hasHandle = _mutex.WaitOne(Timeout.Infinite, false);
                      else
                          _hasHandle = _mutex.WaitOne(timeOut, false);
          
                      if (_hasHandle == false)
                          throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance");
                  }
                  catch (AbandonedMutexException)
                  {
                      _hasHandle = true;
                  }
              }
          
          
              public void Dispose()
              {
                  if (_mutex != null)
                  {
                      if (_hasHandle)
                          _mutex.ReleaseMutex();
                      _mutex.Close();
                  }
              }
          }
          

          【讨论】:

          • 语义上的 (new SingleGlobalInstance(xxx)) 使人们相信每个互斥体都是不同的,而实际上它们都指的是同一个互斥体。创建一个“使用(new MutexLocker(Mutex mutexOpt = null)”会不会更清楚,它创建/默认为每个应用程序仅创建一次的静态互斥锁(可能像您所做的那样隐藏在类中)?此外,“全局”通常意味着“应用程序范围”,而系统是“服务器范围”,我相信这就是命名互斥体。msdn.microsoft.com/en-us/library/hw29w7t1.aspx
          • 消耗SingleGlobalInstance类的超时异常如何处理。在构造实例时抛出异常也是一种好习惯吗?
          • 超时时间为 0 仍应为超时时间为零,而不是无穷大!最好检查 &lt; 0 而不是 &lt;= 0
          • @antistar:我发现在 Dispose 方法中使用 _mutex.Close() 而不是 _mutex.Dispose() 对我有用。该错误是由于试图处置底层的 WaitHandle 造成的。 Mutex.Close() 处理底层资源。
          • 它显示“AppName 已停止工作”。当我尝试打开应用程序的第二个实例时。当用户尝试打开应用程序的第二个实例时,我想将焦点设置在应用程序上。我该怎么做?
          【解决方案6】:

          Mutex 和 WinApi CreateMutex() 都不适合我。

          另一种解决方案:

          static class Program
          {
              [STAThread]
              static void Main()
              {
                  if (SingleApplicationDetector.IsRunning()) {
                      return;
                  }
          
                  Application.Run(new MainForm());
          
                  SingleApplicationDetector.Close();
              }
          }
          

          还有SingleApplicationDetector

          using System;
          using System.Reflection;
          using System.Runtime.InteropServices;
          using System.Security.AccessControl;
          using System.Threading;
          
          public static class SingleApplicationDetector
          {
              public static bool IsRunning()
              {
                  string guid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();
                  var semaphoreName = @"Global\" + guid;
                  try {
                      __semaphore = Semaphore.OpenExisting(semaphoreName, SemaphoreRights.Synchronize);
          
                      Close();
                      return true;
                  }
                  catch (Exception ex) {
                      __semaphore = new Semaphore(0, 1, semaphoreName);
                      return false;
                  }
              }
          
              public static void Close()
              {
                  if (__semaphore != null) {
                      __semaphore.Close();
                      __semaphore = null;
                  }
              }
          
              private static Semaphore __semaphore;
          }
          

          使用 Semaphore 而不是 Mutex 的原因:

          Mutex 类强制执行线程标识,因此互斥锁只能由获取它的线程释放。相比之下,Semaphore 类不强制执行线程标识。

          System.Threading.Mutex

          参考:Semaphore.OpenExisting()

          【讨论】:

          • Semaphore.OpenExistingnew Semaphore 之间可能的竞争条件。
          【解决方案7】:

          如果另一个实例已经在运行,此示例将在 5 秒后退出。

          // unique id for global mutex - Global prefix means it is global to the machine
          const string mutex_id = "Global\\{B1E7934A-F688-417f-8FCB-65C3985E9E27}";
          
          static void Main(string[] args)
          {
          
              using (var mutex = new Mutex(false, mutex_id))
              {
                  try
                  {
                      try
                      {
                          if (!mutex.WaitOne(TimeSpan.FromSeconds(5), false))
                          {
                              Console.WriteLine("Another instance of this program is running");
                              Environment.Exit(0);
                          }
                      }
                      catch (AbandonedMutexException)
                      {
                          // Log the fact the mutex was abandoned in another process, it will still get aquired
                      }
          
                      // Perform your work here.
                  }
                  finally
                  {
                      mutex.ReleaseMutex();
                  }
              }
          }
          

          【讨论】:

            猜你喜欢
            • 2014-09-26
            • 2012-06-05
            • 2021-07-24
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多