Was ist ein gutes Muster für die Verwendung eines globalen Mutex in C #?

377

Die Mutex-Klasse wird sehr missverstanden, und globale Mutexe noch mehr.

Was ist ein gutes, sicheres Muster beim Erstellen globaler Mutexe?

Eine, die funktionieren wird

  • Unabhängig vom Gebietsschema befindet sich mein Computer
  • Wird garantiert, um den Mutex richtig freizugeben
  • Optional hängt nicht für immer, wenn der Mutex nicht erfasst wird
  • Behandelt Fälle, in denen andere Prozesse den Mutex verlassen
Sam Safran
quelle

Antworten:

402

Ich möchte sicherstellen, dass dies da draußen ist, weil es so schwer ist, es richtig zu machen:

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
quelle
1
Möglicherweise möchten Sie usingdas Überprüfen createdNewund Hinzufügen im mutex.Dispose()Inneren weglassen finally. Ich kann es nicht eindeutig erklären (ich weiß nicht den Grund) im Augenblick , aber ich habe mich in eine Situation geraten , wenn mutex.WaitOnewieder truenach createdNewwurde false(I erwerben die Mutex im aktuellen AppDomainund geladen dann ein neuen AppDomainund der gleichen Code ausgeführt von darin).
Sergey.quixoticaxis.Ivanov
1. Tut exitContext = falseetwas in mutex.WaitOne(5000, false)? Es sieht aus wie es nur zu einer Assertion in CoreCLR verursachen könnte , 2. Wenn jemand fragt sich , in Mutex‚s - Konstruktor der Grund, warum initiallyOwnedist falseerklärt teilweise von diesem MSDN - Artikel .
JRH
3
Ein Tipp: Achten Sie bei der Verwendung von Mutex mit ASP.NET darauf: "Die Mutex-Klasse erzwingt die Thread-Identität, sodass ein Mutex nur von dem Thread freigegeben werden kann, der ihn erworben hat. Im Gegensatz dazu erzwingt die Semaphore-Klasse keine Thread-Identität." Eine ASP.NET-Anforderung kann von mehreren Threads bearbeitet werden.
Sam Rueby
Startupnextinstance- Ereignis sicher in VB.NET? nicht in C # docs.microsoft.com/es-es/dotnet/api/…
Kiquenet
Siehe meine Antwort ohne WaitOne. stackoverflow.com/a/59079638/4491768
Wouter
129

Mit der akzeptierten Antwort erstelle ich eine Hilfsklasse, damit Sie sie auf ähnliche Weise wie mit der Lock-Anweisung verwenden können. Ich dachte nur, ich würde teilen.

Verwenden:

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

Und die Helferklasse:

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
quelle
Tolle Arbeit, danke! Zu Ihrer Information: Ich habe die oben beschriebene Dispose-Methode aktualisiert, um die CA2213-Warnung während der Codeanalyse zu verhindern. Der Rest ging gut. Weitere Informationen finden
Pat Hermens
1
Wie gehe ich mit der Timeout-Ausnahme in der Klasse um, die SingleGlobalInstance verwendet? Ist es auch empfehlenswert, beim Erstellen einer Instanz eine Ausnahme auszulösen?
Kiran
3
Ein Timeout von 0 sollte immer noch ein Timeout von Null sein, nicht unendlich! Besser prüfen < 0statt <= 0.
Ygoe
2
@antistar: Ich habe festgestellt, dass die Verwendung _mutex.Close()anstelle _mutex.Dispose()der Dispose-Methode für mich funktioniert. Der Fehler wurde durch den Versuch verursacht, das zugrunde liegende WaitHandle zu entsorgen. Mutex.Close()verfügt über die zugrunde liegenden Ressourcen.
djpMusic
1
Es zeigt "AppName funktioniert nicht mehr." wenn ich versuche, die zweite Instanz der App zu öffnen. Ich möchte den Fokus auf die App legen, wenn der Benutzer versucht, die zweite Instanz der App zu öffnen. Wie kann ich es tun?
Bhaskar
13

Die akzeptierte Antwort enthält eine Race-Bedingung, wenn 2 Prozesse unter 2 verschiedenen Benutzern ausgeführt werden, die gleichzeitig versuchen, den Mutex zu initialisieren. Nachdem der erste Prozess den Mutex initialisiert hat und der zweite Prozess versucht, den Mutex zu initialisieren, bevor der erste Prozess die Zugriffsregel für alle festlegt, wird vom zweiten Prozess eine nicht autorisierte Ausnahme ausgelöst.

Siehe unten für korrigierte Antwort:

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
quelle
8
Beachten Sie, dass dieses Problem jetzt in der akzeptierten Antwort behoben ist.
Van Nguyen
10

Dieses Beispiel wird nach 5 Sekunden beendet, wenn bereits eine andere Instanz ausgeführt wird.

// 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
quelle
10

Weder Mutex noch WinApi CreateMutex () funktionieren bei mir.

Eine alternative Lösung:

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

        Application.Run(new MainForm());

        SingleApplicationDetector.Close();
    }
}

Und die 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;
}

Grund für die Verwendung von Semaphore anstelle von Mutex:

Die Mutex-Klasse erzwingt die Thread-Identität, sodass ein Mutex nur von dem Thread freigegeben werden kann, der ihn erworben hat. Im Gegensatz dazu erzwingt die Semaphore-Klasse keine Thread-Identität.

<< System.Threading.Mutex

Ref: Semaphore.OpenExisting ()

Sol
quelle
7
Mögliche Rennbedingungen zwischen Semaphore.OpenExistingund new Semaphore.
Xmedeko
3

Manchmal hilft das Lernen am Beispiel am meisten. Führen Sie diese Konsolenanwendung in drei verschiedenen Konsolenfenstern aus. Sie werden sehen, dass die Anwendung, die Sie zuerst ausgeführt haben, zuerst den Mutex erhält, während die anderen beiden darauf warten, dass sie an die Reihe kommen. Drücken Sie dann in der ersten Anwendung die Eingabetaste. Sie werden sehen, dass Anwendung 2 jetzt weiter ausgeführt wird, indem Sie den Mutex abrufen. Anwendung 3 wartet jedoch darauf, dass sie an die Reihe kommt. Nachdem Sie in Anwendung 2 die Eingabetaste gedrückt haben, wird Anwendung 3 fortgesetzt. Dies veranschaulicht das Konzept eines Mutex, der einen Codeabschnitt schützt, der nur von einem Thread (in diesem Fall einem Prozess) ausgeführt werden soll, wie beispielsweise das Schreiben in eine Datei.

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

Geben Sie hier die Bildbeschreibung ein

Wahrheitssucher
quelle
0

Ein globaler Mutex soll nicht nur sicherstellen, dass nur eine Instanz einer Anwendung vorhanden ist. Ich persönlich bevorzuge die Verwendung von Microsoft.VisualBasic, um sicherzustellen, dass eine Einzelinstanzanwendung wie unter Was ist der richtige Weg zum Erstellen einer Einzelinstanz-WPF-Anwendung beschrieben?(Antwort von Dale Ragan) ... Ich fand, dass es einfacher ist, beim Start einer neuen Anwendung empfangene Argumente an die erste Einzelinstanzanwendung zu übergeben.

In Bezug auf einen früheren Code in diesem Thread würde ich es jedoch vorziehen, nicht jedes Mal einen Mutex zu erstellen, wenn ich eine Sperre dafür haben möchte. Es könnte für eine einzelne Instanzanwendung in Ordnung sein, aber in einer anderen Verwendung scheint es mir übertrieben zu sein.

Deshalb schlage ich stattdessen diese Implementierung vor:

Verwendungszweck:

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

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

    }
}

Ein Kellner

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
quelle
0

Eine Lösung (für WPF) ohne WaitOne, da sie eine AbandonedMutexException verursachen kann. Diese Lösung verwendet den Mutex-Konstruktor, der den booleschen Wert createdNew zurückgibt, um zu überprüfen, ob der Mutex bereits erstellt wurde. Außerdem wird die GUID GetType () verwendet, sodass beim Umbenennen einer ausführbaren Datei nicht mehrere Instanzen zulässig sind.

Globaler und lokaler Mutex siehe Hinweis 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();
    }
}

Da Mutex IDisposable implementiert, wird es automatisch freigegeben. Der Vollständigkeit halber rufen Sie Folgendes auf:

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

Verschieben Sie alles in eine Basisklasse und fügen Sie die allowEveryoneRule aus der akzeptierten Antwort hinzu. Außerdem wurde ReleaseMutex hinzugefügt, obwohl es nicht wirklich benötigt wird, da es automatisch vom Betriebssystem freigegeben wird (was wäre, wenn die Anwendung abstürzt und niemals ReleaseMutex aufruft, müssten Sie neu starten?).

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
quelle