¿Cuál es un buen patrón para usar un Global Mutex en C #?

377

La clase Mutex es muy incomprendida, y los mutex globales aún más.

¿Qué patrón es bueno y seguro de usar al crear mutexes globales?

Uno que funcione

  • Independientemente de la ubicación en la que se encuentre mi máquina
  • Está garantizado para liberar el mutex correctamente
  • Opcionalmente, no se cuelga para siempre si no se adquiere el mutex
  • Se ocupa de casos donde otros procesos abandonan el mutex
Sam Azafrán
fuente

Respuestas:

402

Quiero asegurarme de que esto esté ahí afuera, porque es muy difícil hacerlo bien:

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();
        }
    }
}
Sam Saffron
fuente
1
es posible que desee omitir usingpara verificar createdNewy agregar mutex.Dispose()dentro finally. No puedo explicarlo con claridad (no sé la razón), pero en este momento me he llegado a una situación en la que mutex.WaitOneregresó truedespués createdNewse convirtió en false(adquirí la exclusión mutua en la corriente AppDomainy después se carga un nuevo AppDomainy ejecutado el mismo código de dentro de ella).
Sergey.quixoticaxis.Ivanov
1. ¿Hace exitContext = falsealgo en mutex.WaitOne(5000, false)? Parece que sólo podría causar una aserción en CoreCLR , 2. Si alguien se pregunta, en el Mutexconstructor 's, la razón por la cual initiallyOwnedes falsese explica en parte por este artículo de MSDN .
jrh
3
Un consejo: tenga cuidado al usar Mutex con ASP.NET: "La clase Mutex impone la identidad del subproceso, por lo que un mutex solo puede ser liberado por el subproceso que lo adquirió. Por el contrario, la clase Semaphore no aplica la identidad del subproceso". Una solicitud ASP.NET puede ser atendida por múltiples hilos.
Sam Rueby
¿ Suceso de inicio de instancia de forma segura en VB.NET? no en C # docs.microsoft.com/es-es/dotnet/api/…
Kiquenet
Vea mi respuesta sin usar WaitOne. stackoverflow.com/a/59079638/4491768
Wouter el
129

Usando la respuesta aceptada, creo una clase auxiliar para que pueda usarla de la misma manera que usaría la instrucción Lock. Solo pensé en compartir.

Utilizar:

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

Y la clase auxiliar:

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();
        }
    }
}
deepee1
fuente
Impresionante trabajo, gracias! FYI: He actualizado el método de eliminación anterior para evitar la advertencia CA2213 durante el análisis de código. El resto pasó bien. Para obtener más detalles, consulte msdn.microsoft.com/query/…
Pat Hermens
1
¿Cómo manejo la excepción de tiempo de espera en la clase que consume SingleGlobalInstance. ¿También es una buena práctica lanzar una excepción al construir una instancia?
kiran
3
¡Un tiempo de espera de 0 debería ser un tiempo de espera de cero, no infinito! Mejor comprobar en < 0lugar de <= 0.
ygoe
2
@antistar: descubrí que usar en _mutex.Close()lugar del _mutex.Dispose()método Dispose funcionó para mí. El error fue causado al intentar deshacerse de WaitHandle subyacente. Mutex.Close()dispone de los recursos subyacentes.
djpMusic
1
Muestra "AppName ha dejado de funcionar". cuando intento abrir la segunda instancia de la aplicación. Quiero establecer el foco en la aplicación cuando el usuario intenta abrir la segunda instancia de la aplicación. ¿Cómo puedo hacerlo?
Bhaskar
13

Hay una condición de carrera en la respuesta aceptada cuando 2 procesos se ejecutan bajo 2 usuarios diferentes que intentan inicializar el mutex al mismo tiempo. Después de que el primer proceso inicialice el mutex, si el segundo proceso intenta inicializar el mutex antes de que el primer proceso establezca la regla de acceso para todos, el segundo proceso generará una excepción no autorizada.

Vea a continuación la respuesta corregida:

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 );

    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);

        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 the mutex was abandoned in another process, it will still get aquired
                hasHandle = true;
            }

            // Perform your work here.
        }
        finally
        {
            // edited by acidzombie24, added if statemnet
            if(hasHandle)
                mutex.ReleaseMutex();
        }
    }
}
Van Nguyen
fuente
8
Tenga en cuenta que este problema ahora está solucionado en la respuesta aceptada.
Van Nguyen el
10

Este ejemplo saldrá después de 5 segundos si ya se está ejecutando otra instancia.

// 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();
        }
    }
}
Liam
fuente
10

Ni Mutex ni WinApi CreateMutex () funcionan para mí.

Una solución alternativa:

static class Program
{
    [STAThread]
    static void Main()
    {
        if (SingleApplicationDetector.IsRunning()) {
            return;
        }

        Application.Run(new MainForm());

        SingleApplicationDetector.Close();
    }
}

Y el 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;
}

Razón para usar Semaphore en lugar de Mutex:

La clase Mutex impone la identidad del hilo, por lo que un mutex solo puede ser liberado por el hilo que lo adquirió. Por el contrario, la clase Semaphore no impone la identidad del hilo.

<< System.Threading.Mutex

Ref: Semaphore.OpenExisting ()

Sol
fuente
77
Posibles condiciones de carrera entre Semaphore.OpenExistingy new Semaphore.
xmedeko
3

A veces, aprender con el ejemplo ayuda más. Ejecute esta aplicación de consola en tres ventanas de consola diferentes. Verá que la aplicación que ejecutó primero adquiere el mutex primero, mientras que los otros dos esperan su turno. Luego presione Intro en la primera aplicación, verá que la aplicación 2 ahora continúa ejecutándose adquiriendo el mutex, sin embargo, la aplicación 3 está esperando su turno. Después de presionar enter en la aplicación 2, verá que la aplicación 3 continúa. Esto ilustra el concepto de un mutex que protege una sección de código que se ejecutará solo por un hilo (en este caso, un proceso) como escribir en un archivo como ejemplo.

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
        }
    }
}

ingrese la descripción de la imagen aquí

Buscador de la verdad
fuente
0

Un Mutex global no es solo para asegurarse de tener solo una instancia de una aplicación. Personalmente prefiero usar Microsoft.VisualBasic para garantizar una aplicación de instancia única como se describe en ¿Cuál es la forma correcta de crear una aplicación WPF de instancia única? (Respuesta de Dale Ragan) ... descubrí que es más fácil pasar los argumentos recibidos en el inicio de la nueva aplicación a la aplicación de instancia única inicial.

Pero con respecto a algún código anterior en este hilo, preferiría no crear un Mutex cada vez que quiera tener un bloqueo. Podría estar bien para una aplicación de instancia única, pero en otro uso me parece que es excesivo.

Es por eso que sugiero esta implementación en su lugar:

Uso:

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

using (GlobalMutexAccessEMTP.GetAwaiter())
{
    ...
}   

Mutex Global Wrapper:

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();
            }
        }

        // ************************************************************************

    }
}

Un mesero

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
    }
}
Eric Ouellet
fuente
0

Una solución (para WPF) sin WaitOne porque puede causar una excepción AbandonedMutexException. Esta solución utiliza el constructor Mutex que devuelve el booleano createdNew para verificar si el mutex ya está creado. También utiliza el GUID GetType (), por lo que renombrar un ejecutable no permite múltiples instancias.

Mutex global vs local ver nota en: 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();
    }
}

Debido a que Mutex implementa IDisposable, se libera automáticamente, pero para completar, llame a disponer:

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

Mueva todo a una clase base y agregue allowEveryoneRule de la respuesta aceptada. También agregó ReleaseMutex, aunque no parece que sea realmente necesario porque el sistema operativo lo lanza automáticamente (¿qué sucede si la aplicación se bloquea y nunca llama a ReleaseMutex, necesitaría reiniciar?).

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();
        }
    }
}
Wouter
fuente