Qual è un buon modello per usare un Global Mutex in C #?


377

La classe Mutex è molto fraintesa e i mutex globali lo sono ancora di più.

Qual è il modello buono e sicuro da usare durante la creazione di mutex globali?

Uno che funzionerà

  • Indipendentemente dalla locale in cui si trova la mia macchina
  • È garantito per rilasciare correttamente il mutex
  • Opzionalmente non si blocca per sempre se il mutex non viene acquisito
  • Si occupa di casi in cui altri processi abbandonano il mutex

Risposte:


402

Voglio assicurarmi che questo sia là fuori, perché è così difficile ottenere il giusto:

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

1
potresti voler omettere usingdi controllare createdNewe aggiungere mutex.Dispose()all'interno finally. Non posso spiegarlo chiaramente (non conosco il motivo) in questo momento ma mi sono ritrovato in una situazione quando è mutex.WaitOnetornato truedopo essere createdNewdiventato false(ho acquisito il mutex nella corrente AppDomaine quindi caricato un nuovo AppDomained eseguito lo stesso codice da al suo interno).
Sergey.quixoticaxis.Ivanov

1. Fa exitContext = falsequalcosa dentro mutex.WaitOne(5000, false)? Sembra che potrebbe causare solo un'asserzione in CoreCLR , 2. Se qualcuno sta chiedendo, a Mutex's costruttore, il motivo per cui initiallyOwnedè falseparzialmente spiegato da questo articolo di MSDN .
jr

3
Un consiglio: fai attenzione usando Mutex con ASP.NET: "La classe Mutex impone l'identità del thread, quindi un mutex può essere rilasciato solo dal thread che lo ha acquisito. Al contrario, la classe Semaphore non impone l'identità del thread". Una richiesta ASP.NET può essere gestita da più thread.
Sam Rueby,

evento startupnextinstance sicuro in VB.NET? non in C # docs.microsoft.com/es-es/dotnet/api/…
Kiquenet

Vedi la mia risposta senza usare WaitOne. stackoverflow.com/a/59079638/4491768
Wouter,

129

Usando la risposta accettata creo una classe helper in modo che tu possa usarla in un modo simile a quello che utilizzeresti nell'istruzione Lock. Ho pensato di condividere.

Uso:

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

E la classe di aiuto:

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

Ottimo lavoro, grazie! Cordiali saluti: Ho aggiornato il metodo Dispose sopra per impedire l'avviso CA2213 durante l'analisi del codice. Il resto è andato bene. Per maggiori dettagli, consultare msdn.microsoft.com/query/…
Pat Hermens il

1
Come gestisco l'eccezione di timeout nella classe che utilizza SingleGlobalInstance. Inoltre è buona norma generare un'eccezione durante la costruzione di un'istanza?
Kiran,

3
Un timeout di 0 dovrebbe comunque essere un timeout di zero, non di infinito! Meglio controllare < 0invece di <= 0.
giovedì

2
@antistar: ho scoperto che l'utilizzo al _mutex.Close()posto del _mutex.Dispose()metodo Dispose ha funzionato per me. L'errore è stato causato dal tentativo di eliminare il WaitHandle sottostante. Mutex.Close()dispone delle risorse sottostanti.
djpMusic

1
Mostra "AppName ha smesso di funzionare." quando provo ad aprire la seconda istanza dell'app. Voglio focalizzare l'attenzione sull'app quando l'utente tenta di aprire la seconda istanza dell'app. Come posso farlo?
Bhaskar,

13

Esiste una condizione di competizione nella risposta accettata quando 2 processi in esecuzione su 2 utenti diversi tentano di inizializzare il mutex contemporaneamente. Dopo che il primo processo ha inizializzato il mutex, se il secondo processo tenta di inizializzare il mutex prima che il primo processo imposti la regola di accesso a tutti, il secondo processo genererà un'eccezione non autorizzata.

Vedi sotto per la risposta corretta:

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

8
Nota che questo problema è stato risolto nella risposta accettata.
Van Nguyen,

10

Questo esempio verrà chiuso dopo 5 secondi se è già in esecuzione un'altra istanza.

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

10

Né Mutex né WinApi CreateMutex () funzionano per me.

Una soluzione alternativa:

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

        Application.Run(new MainForm());

        SingleApplicationDetector.Close();
    }
}

E il 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;
}

Motivo per usare Semaphore invece di Mutex:

La classe Mutex applica l'identità del thread, quindi un mutex può essere rilasciato solo dal thread che lo ha acquisito. Al contrario, la classe Semaphore non impone l'identità del thread.

<< System.Threading.Mutex

Rif: Semaphore.OpenExisting ()


7
Possibili condizioni di gara tra Semaphore.OpenExistinge new Semaphore.
xmedeko,

3

A volte l'apprendimento con l'esempio aiuta di più. Esegui questa applicazione console in tre diverse finestre console. Vedrai che l'applicazione che hai eseguito per prima acquisisce prima il mutex, mentre gli altri due stanno aspettando il loro turno. Quindi premi Invio nella prima applicazione, vedrai che l'applicazione 2 ora continua a funzionare acquisendo il mutex, tuttavia l'applicazione 3 sta aspettando il suo turno. Dopo aver premuto Invio nell'applicazione 2, vedrai che l'applicazione 3 continua. Questo illustra il concetto di un mutex che protegge una sezione di codice che deve essere eseguita da un solo thread (in questo caso un processo) come scrivere in un file come esempio.

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

inserisci qui la descrizione dell'immagine


0

Un Mutex globale non è solo per garantire di avere solo un'istanza di un'applicazione. Personalmente preferisco utilizzare Microsoft.VisualBasic per garantire un'applicazione a istanza singola come descritto in Qual è il modo corretto di creare un'applicazione WPF a istanza singola? (Risposta di Dale Ragan) ... Ho scoperto che è più facile passare gli argomenti ricevuti all'avvio della nuova applicazione all'applicazione iniziale a singola istanza.

Ma per quanto riguarda un codice precedente in questo thread, preferirei non creare un Mutex ogni volta che voglio avere un blocco su di esso. Potrebbe andare bene per un'applicazione a singola istanza ma in altri usi mi sembra eccessivo.

Ecco perché suggerisco invece questa implementazione:

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 cameriere

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

0

Una soluzione (per WPF) senza WaitOne perché può causare una AbandonedMutexException. Questa soluzione utilizza il costruttore Mutex che restituisce il nuovo valore booleano per verificare se il mutex è già stato creato. Utilizza anche il GUID GetType (), pertanto la ridenominazione di un eseguibile non consente più istanze.

Mutex globale vs locale vedi nota in: 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();
    }
}

Poiché Mutex implementa IDisposable, viene rilasciato automaticamente ma per completezza chiamare:

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

Sposta tutto in una classe base e aggiungi allowEveryoneRule dalla risposta accettata. Inoltre ha aggiunto ReleaseMutex sebbene non sembri realmente necessario perché viene rilasciato automaticamente dal sistema operativo (cosa succede se l'applicazione si arresta in modo anomalo e non chiama mai ReleaseMutex sarebbe necessario riavviare?).

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();
        }
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.