Qual é um bom padrão para usar um Global Mutex em C #?

377

A classe Mutex é muito incompreendida e a Global mutexes ainda mais.

Qual é o padrão bom e seguro a ser usado ao criar mutexes globais?

Um que irá funcionar

  • Independentemente da localidade em que minha máquina está
  • É garantido que libere o mutex corretamente
  • Opcionalmente, não é interrompido para sempre se o mutex não for adquirido
  • Lida com casos em que outros processos abandonam o mutex
Sam Saffron
fonte

Respostas:

402

Quero ter certeza de que isso existe, porque é muito difícil acertar:

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
fonte
11
convém omitir usingpara verificar createdNewe adicionar mutex.Dispose()dentro finally. Não posso explicar claramente (não sei o motivo) no momento, mas me deparei com uma situação em que mutex.WaitOneretornei truedepois que me createdNewtornei false(adquiri o mutex no atual AppDomaine carreguei um novo AppDomaine executei o mesmo código de dentro dele).
precisa saber é o seguinte
1. Faz exitContext = falsealguma coisa mutex.WaitOne(5000, false)? Parece que ele só poderia causar uma declaração em CoreCLR , 2. Se alguém está se perguntando, em Mutex's construtor, a razão pela qual initiallyOwnedé falseparcialmente explicada por este artigo MSDN .
JRH
3
Uma dica: cuidado ao usar o Mutex com o ASP.NET: "A classe Mutex impõe a identidade do encadeamento, portanto, um mutex pode ser liberado apenas pelo encadeamento que o adquiriu. Por outro lado, a classe Semaphore não impõe a identidade do encadeamento.". Uma solicitação do ASP.NET pode ser atendida por vários threads.
Sam Rueby
evento startupnextinstance com segurança no VB.NET? não em C # docs.microsoft.com/es-es/dotnet/api/…
Kiquenet
Veja minha resposta sem usar o WaitOne. stackoverflow.com/a/59079638/4491768
Wouter
129

Usando a resposta aceita, crio uma classe auxiliar para que você possa usá-la da mesma maneira que usaria a instrução Lock. Só pensei em compartilhar.

Usar:

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

E a classe 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
fonte
Trabalho incrível, obrigado! FYI: Atualizei o método Dispose acima para impedir o aviso CA2213 durante a análise de código. O resto passou bem. Para obter mais detalhes, consulte msdn.microsoft.com/query/…
Pat Hermens
11
Como lidar com a exceção de tempo limite na classe que consome SingleGlobalInstance. Também é uma boa prática lançar exceção ao construir uma instância?
22414
3
Um tempo limite de 0 ainda deve ser zero, não infinito! Melhor verificar em < 0vez de <= 0.
ygoe 25/02
2
@antistar: Descobri que usar o método Dispose ao _mutex.Close()invés de _mutex.Dispose()funcionou para mim. O erro foi causado ao tentar descartar o WaitHandle subjacente. Mutex.Close()dispõe dos recursos subjacentes.
DjpMusic
11
Ele mostra "AppName parou de funcionar". quando tento abrir a segunda instância do aplicativo. Desejo definir o foco no aplicativo quando o usuário tenta abrir a segunda instância do aplicativo. Como eu posso fazer isso?
Bhaskar
13

Há uma condição de corrida na resposta aceita quando 2 processos sendo executados com menos de 2 usuários diferentes tentando inicializar o mutex ao mesmo tempo. Após o primeiro processo inicializar o mutex, se o segundo processo tentar inicializar o mutex antes que o primeiro processo defina a regra de acesso a todos, uma exceção não autorizada será lançada pelo segundo processo.

Veja abaixo a resposta corrigida:

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
fonte
8
Observe que agora esse problema foi corrigido na resposta aceita.
Van Nguyen
10

Este exemplo será encerrado após 5 segundos se outra instância já estiver em execução.

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

Nem o Mutex nem o WinApi CreateMutex () funcionam para mim.

Uma solução alternativa:

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

        Application.Run(new MainForm());

        SingleApplicationDetector.Close();
    }
}

E o 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ão para usar o Semáforo em vez do Mutex:

A classe Mutex aplica a identidade do encadeamento, portanto, um mutex pode ser liberado apenas pelo encadeamento que o adquiriu. Por outro lado, a classe Semáforo não impõe a identidade do encadeamento.

<< System.Threading.Mutex

Ref: Semaphore.OpenExisting ()

Sol
fonte
7
Possíveis condições de corrida entre Semaphore.OpenExistinge new Semaphore.
xmedeko
3

Às vezes, aprender pelo exemplo ajuda mais. Execute este aplicativo de console em três janelas diferentes. Você verá que o aplicativo que você executou primeiro adquire o mutex primeiro, enquanto os outros dois estão esperando a vez deles. Em seguida, pressione enter no primeiro aplicativo. Você verá que o aplicativo 2 continua em execução adquirindo o mutex, mas o aplicativo 3 está aguardando sua vez. Depois de pressionar enter no aplicativo 2, você verá que o aplicativo 3 continua. Isso ilustra o conceito de um mutex que protege uma seção de código a ser executada apenas por um encadeamento (neste caso, um processo) como gravar em um arquivo como exemplo.

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

insira a descrição da imagem aqui

TruthSeeker
fonte
0

Um Mutex global não serve apenas para garantir a existência de apenas uma instância de um aplicativo. Pessoalmente, prefiro usar o Microsoft.VisualBasic para garantir um aplicativo de instância única, como descrito em Qual é a maneira correta de criar um aplicativo WPF de instância única? (Resposta de Dale Ragan) ... Achei mais fácil passar argumentos recebidos na inicialização do novo aplicativo para o aplicativo de instância única inicial.

Mas, com relação a algum código anterior desse segmento, eu preferiria não criar um Mutex toda vez que eu quisesse ter um bloqueio nele. Pode ser bom para um aplicativo de instância única, mas em outro uso parece-me um exagero.

É por isso que sugiro esta implementação:

Uso:

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

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

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

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

    }
}

Um garçom

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

Uma solução (para WPF) sem WaitOne porque pode causar uma AbandonedMutexException. Esta solução usa o construtor Mutex que retorna o booleano createdNew para verificar se o mutex já foi criado. Ele também usa o GetType (). GUID, portanto, renomear um executável não permite várias instâncias.

Mutex global x local, consulte a nota em: 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();
    }
}

Como o Mutex implementa o IDisposable, ele é liberado automaticamente, mas, para completar, chame descarte:

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

Mova tudo para uma classe base e adicione o allowEveryoneRule da resposta aceita. Também foi adicionado o ReleaseMutex, embora não pareça realmente necessário porque é lançado automaticamente pelo sistema operacional (e se o aplicativo falhar e nunca chamar o ReleaseMutex, você precisaria 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
fonte