Jaki jest dobry wzorzec korzystania z Global Mutex w C #?

377

Klasa Mutex jest bardzo źle rozumiana, a globalne muteksy jeszcze bardziej.

Jaki dobry, bezpieczny wzorzec należy stosować podczas tworzenia globalnych muteksów?

Taki, który zadziała

  • Bez względu na lokalizację, w której znajduje się moja maszyna
  • Gwarantuje prawidłowe uwolnienie muteksu
  • Opcjonalnie nie zawiesza się na zawsze, jeśli muteks nie zostanie przejęty
  • Zajmuje się przypadkami, w których inne procesy porzucają muteks
Sam Saffron
źródło

Odpowiedzi:

402

Chcę się upewnić, że to tam jest, ponieważ tak trudno jest to naprawić:

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
źródło
1
możesz pominąć usingzaznaczenie createdNewi dodanie do mutex.Dispose()środka finally. Nie mogę tego teraz jasno wyjaśnić (nie znam powodu), ale doszedłem do sytuacji, gdy mutex.WaitOnewróciłem truepo tym, jak się createdNewstał false(nabyłem muteks w bieżącym, AppDomaina następnie załadowałem nowy AppDomaini wykonałem ten sam kod z w tym).
Sergey.quixoticaxis.Ivanov
1. Czy exitContext = falsecoś robi mutex.WaitOne(5000, false)? Wygląda na to, że może to spowodować tylko twierdzenie w CoreCLR , 2. Jeśli ktoś zastanawia się, w Mutexkonstruktorze, powód, dla którego initiallyOwnedjest falseto częściowo wyjaśnione w tym artykule MSDN .
2017 r. O
3
Wskazówka: uważaj przy użyciu Mutex z ASP.NET: „Klasa Mutex wymusza tożsamość wątku, więc muteks może zostać zwolniony tylko przez wątek, który go pozyskał. Natomiast klasa Semaphore nie wymusza tożsamości wątku.”. Żądanie ASP.NET może być obsługiwane przez wiele wątków.
Sam Rueby
Zdarzenie startupnextinstance bezpiecznie w VB.NET? nie w C # docs.microsoft.com/es-es/dotnet/api/…
Kiquenet
Zobacz moją odpowiedź bez używania WaitOne. stackoverflow.com/a/59079638/4491768
Wouter
129

Korzystając z zaakceptowanej odpowiedzi, tworzę klasę pomocnika, abyś mógł użyć jej w podobny sposób, jak przy użyciu instrukcji Lock. Pomyślałem, że się podzielę.

Posługiwać się:

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

I klasa pomocnicza:

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
źródło
Świetna robota, dzięki! FYI: Zaktualizowałem powyższą metodę Dispose, aby zapobiec ostrzeżeniu CA2213 podczas analizy kodu. Reszta minęła dobrze. Aby uzyskać więcej informacji, sprawdź msdn.microsoft.com/query/…
Pat Hermens,
1
Jak obsłużyć wyjątek limitu czasu w klasie, która korzysta z SingleGlobalInstance. Czy dobrą praktyką jest zgłaszanie wyjątku podczas konstruowania instancji?
kiran
3
Limit czasu równy 0 powinien nadal być limitem równym zero, a nie nieskończoności! Lepiej sprawdź < 0zamiast <= 0.
ygoe
2
@antistar: Odkryłem, że zastosowanie _mutex.Close()zamiast _mutex.Dispose()metody Dispose działało dla mnie. Błąd był spowodowany próbą pozbycia się bazowego WaitHandle. Mutex.Close()pozbywa się podstawowych zasobów.
djpMusic
1
Pokazuje „AppName przestała działać”. kiedy próbuję otworzyć drugą instancję aplikacji. Chcę ustawić fokus na aplikacji, gdy użytkownik próbuje otworzyć drugie wystąpienie aplikacji. Jak mogę to zrobić?
Bhaskar
13

W zaakceptowanej odpowiedzi występuje warunek wyścigu, gdy 2 procesy uruchomione przez 2 różnych użytkowników próbują zainicjować muteks w tym samym czasie. Po pierwszym zainicjowaniu muteksu przez pierwszy proces, jeśli drugi proces spróbuje zainicjować muteks, zanim pierwszy proces ustawi dla wszystkich regułę dostępu, drugi proces zgłosi nieautoryzowany wyjątek.

Poprawiona odpowiedź znajduje się poniżej:

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
źródło
8
Uwaga: ten problem został już rozwiązany w zaakceptowanej odpowiedzi.
Van Nguyen,
10

Ten przykład zakończy się po 5 sekundach, jeśli inna instancja jest już uruchomiona.

// 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
źródło
10

Ani Mutex, ani WinApi CreateMutex () nie działa dla mnie.

Alternatywne rozwiązanie:

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

        Application.Run(new MainForm());

        SingleApplicationDetector.Close();
    }
}

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

Powód, aby używać Semafora zamiast Mutex:

Klasa Mutex wymusza tożsamość wątku, więc muteks może zostać zwolniony tylko przez wątek, który go pozyskał. Natomiast klasa Semaphore nie wymusza tożsamości wątku.

<< System.Threading.Mutex

Ref: Semaphore.OpenExisting ()

sol
źródło
7
Możliwe warunki wyścigu między Semaphore.OpenExistingi new Semaphore.
xmedeko
3

Czasami uczenie się na przykładzie najbardziej pomaga. Uruchom tę aplikację konsoli w trzech różnych oknach konsoli. Zobaczysz, że aplikacja, którą najpierw uruchomiłeś, najpierw nabywa muteks, podczas gdy pozostałe dwie czekają na swoją kolej. Następnie naciśnij klawisz Enter w pierwszej aplikacji, zobaczysz, że aplikacja 2 kontynuuje działanie po zdobyciu muteksu, jednak aplikacja 3 czeka na swoją kolej. Po naciśnięciu klawisza Enter w aplikacji 2 zobaczysz, że aplikacja 3 trwa. Ilustruje to koncepcję muteksu chroniącego sekcję kodu, która ma być wykonywana tylko przez jeden wątek (w tym przypadku proces), np. Zapis do pliku jako przykład.

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

wprowadź opis zdjęcia tutaj

Poszukiwacz prawdy
źródło
0

Globalny Mutex ma zapewnić nie tylko jedną instancję aplikacji. Osobiście wolę używać Microsoft.VisualBasic, aby zapewnić aplikację z pojedynczą instancją, jak opisano w Jaki jest prawidłowy sposób tworzenia aplikacji WPF z jedną instancją? (Odpowiedź Dale'a Ragana) ... Odkryłem, że łatwiej jest przekazywać argumenty otrzymane podczas uruchamiania nowej aplikacji do początkowej aplikacji pojedynczej instancji.

Ale jeśli chodzi o niektóre poprzednie kody w tym wątku, wolałbym nie tworzyć Mutex za każdym razem, gdy chcę mieć blokadę. Może to być w porządku dla aplikacji z pojedynczą instancją, ale w innym zastosowaniu wydaje mi się, że ma nadmiar.

Dlatego zamiast tego sugeruję tę implementację:

Stosowanie:

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

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

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

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

    }
}

Kelner

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
źródło
0

Rozwiązanie (dla WPF) bez WaitOne, ponieważ może powodować wyjątek AbandonedMutexException. To rozwiązanie korzysta z konstruktora Mutex, który zwraca wartość utworzonej wartości logicznej, aby sprawdzić, czy muteks jest już utworzony. Wykorzystuje również GetType (). GUID, więc zmiana nazwy pliku wykonywalnego nie pozwala na wiele wystąpień.

Muteks globalny a lokalny patrz uwaga w: 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();
    }
}

Ponieważ Mutex implementuje IDisposable, jest zwalniany automatycznie, ale dla kompletności wywołaj dispose:

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

Przenieś wszystko do klasy bazowej i dodaj allowEveryoneRule z zaakceptowanej odpowiedzi. Dodałem także ReleaseMutex, choć nie wygląda na to, żeby był naprawdę potrzebny, ponieważ jest uwalniany automatycznie przez system operacyjny (co jeśli aplikacja się zawiesi i nigdy nie wywoła ReleaseMutex, czy konieczne byłoby ponowne uruchomienie?).

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
źródło