Quel est un bon modèle pour utiliser un Global Mutex en C #?

377

La classe Mutex est très mal comprise, et les mutex mondiaux encore plus.

Quel modèle bon et sûr utiliser lors de la création de mutex globaux?

Celui qui fonctionnera

  • Quel que soit le lieu où se trouve ma machine
  • Est garanti pour libérer le mutex correctement
  • Optionnellement ne se bloque pas pour toujours si le mutex n'est pas acquis
  • Traite les cas où d'autres processus abandonnent le mutex
Sam Saffron
la source

Réponses:

402

Je veux m'assurer que c'est là-bas, car il est si difficile de bien faire les choses:

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
la source
1
vous voudrez peut-être omettre usingde vérifier createdNewet d'ajouter à l' mutex.Dispose()intérieur finally. Je ne peux pas l'expliquer clairement (je ne connais pas la raison) en ce moment, mais je me suis retrouvé dans une situation où je suis mutex.WaitOnerevenu trueaprès être createdNewdevenu false(j'ai acquis le mutex dans le courant AppDomain, puis j'ai chargé un nouveau AppDomainet j'ai exécuté le même code à partir de à l'intérieur).
Sergey.quixoticaxis.Ivanov
1. Est exitContext = false-ce que ça fait quelque chose mutex.WaitOne(5000, false)? On dirait qu'il ne pouvait provoquer une assertion dans CoreCLR , 2. Si se demander, dans ce quelqu'un Mutexconstructeur de », la raison pour laquelle initiallyOwnedest falsepartiellement expliquée par cet article MSDN .
1er
3
Un conseil: faites attention à utiliser Mutex avec ASP.NET: "La classe Mutex applique l'identité du thread, donc un mutex ne peut être publié que par le thread qui l'a acquis. En revanche, la classe Semaphore n'applique pas l'identité du thread.". Une demande ASP.NET peut être traitée par plusieurs threads.
Sam Rueby
événement startupnextinstance en toute sécurité dans VB.NET? pas en C # docs.microsoft.com/es-es/dotnet/api/…
Kiquenet
Voir ma réponse sans utiliser WaitOne. stackoverflow.com/a/59079638/4491768
Wouter
129

En utilisant la réponse acceptée, je crée une classe d'assistance afin que vous puissiez l'utiliser de la même manière que vous utiliseriez l'instruction Lock. Je pensais juste partager.

Utilisation:

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

Et la classe d'assistance:

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
la source
Super travail, merci! Pour info: j'ai mis à jour la méthode Dispose ci-dessus pour éviter l'avertissement CA2213 lors de l'analyse de code. Le reste s'est bien passé. Pour plus de détails, consultez msdn.microsoft.com/query/…
Pat Hermens
1
Comment gérer l'exception de délai d'expiration dans la classe qui consomme SingleGlobalInstance. Est-ce également une bonne pratique de lever une exception lors de la construction d'une instance?
kiran
3
Un timeout de 0 devrait toujours être un timeout de zéro, pas l'infini! Mieux vaut vérifier au < 0lieu de <= 0.
ygoe
2
@antistar: J'ai trouvé que l'utilisation _mutex.Close()au lieu de _mutex.Dispose()la méthode Dispose fonctionnait pour moi. L'erreur a été provoquée en essayant de supprimer le WaitHandle sous-jacent. Mutex.Close()dispose des ressources sous-jacentes.
djpMusic
1
Il montre "AppName a cessé de fonctionner." lorsque j'essaie d'ouvrir la deuxième instance de l'application. Je souhaite définir le focus sur l'application lorsque l'utilisateur essaie d'ouvrir la deuxième instance de l'application. Comment puis-je le faire?
Bhaskar
13

Il y a une condition de concurrence critique dans la réponse acceptée lorsque 2 processus s'exécutant sous 2 utilisateurs différents tentent d'initialiser le mutex en même temps. Une fois que le premier processus a initialisé le mutex, si le deuxième processus tente d'initialiser le mutex avant que le premier processus ne définisse la règle d'accès pour tout le monde, une exception non autorisée sera levée par le deuxième processus.

Voir ci-dessous pour la réponse corrigée:

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
la source
8
Notez que ce problème est désormais résolu dans la réponse acceptée.
Van Nguyen
10

Cet exemple se fermera après 5 secondes si une autre instance est déjà en cours d'exécution.

// 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
la source
10

Ni Mutex ni WinApi CreateMutex () ne fonctionnent pour moi.

Une solution alternative:

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

        Application.Run(new MainForm());

        SingleApplicationDetector.Close();
    }
}

Et le 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;
}

Raison d'utiliser Semaphore au lieu de Mutex:

La classe Mutex applique l'identité du thread, donc un mutex ne peut être publié que par le thread qui l'a acquis. En revanche, la classe Semaphore n'applique pas l'identité de thread.

<< System.Threading.Mutex

Réf: Semaphore.OpenExisting ()

sol
la source
7
Conditions de course possibles entre Semaphore.OpenExistinget new Semaphore.
xmedeko
3

Parfois, l'apprentissage par l'exemple aide le plus. Exécutez cette application console dans trois fenêtres de console différentes. Vous verrez que l'application que vous avez exécutée en premier acquiert le mutex en premier, tandis que les deux autres attendent leur tour. Appuyez ensuite sur Entrée dans la première application, vous verrez que l'application 2 continue maintenant de fonctionner en acquérant le mutex, mais l'application 3 attend son tour. Après avoir appuyé sur Entrée dans l'application 2, vous verrez que l'application 3 continue. Cela illustre le concept d'un mutex protégeant une section de code à exécuter uniquement par un thread (dans ce cas, un processus) comme l'écriture dans un fichier à titre d'exemple.

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

entrez la description de l'image ici

TruthSeeker
la source
0

Un Mutex mondial ne consiste pas seulement à s'assurer de n'avoir qu'une seule instance d'une application. Personnellement, je préfère utiliser Microsoft.VisualBasic pour garantir une application à instance unique comme décrit dans Quelle est la bonne façon de créer une application WPF à instance unique? (Réponse de Dale Ragan) ... J'ai trouvé qu'il était plus facile de transmettre les arguments reçus au démarrage d'une nouvelle application à l'application initiale à instance unique.

Mais en ce qui concerne un code précédent dans ce fil, je préférerais ne pas créer de Mutex chaque fois que je veux avoir un verrou dessus. Cela pourrait être bien pour une application à instance unique, mais dans d'autres utilisations, il me semble que c'est trop.

C'est pourquoi je suggère plutôt cette implémentation:

Usage:

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 serveur

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
la source
0

Une solution (pour WPF) sans WaitOne car elle peut provoquer une AbandonedMutexException. Cette solution utilise le constructeur Mutex qui renvoie le booléen createdNew pour vérifier si le mutex est déjà créé. Il utilise également le GetType (). GUID donc renommer un exécutable ne permet pas plusieurs instances.

Mutex global vs local voir note dans: 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();
    }
}

Étant donné que Mutex implémente IDisposable, il est libéré automatiquement, mais pour être complet, appelez dispose:

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

Déplacez tout dans une classe de base et ajoutez le allowEveryoneRule de la réponse acceptée. Ajout également de ReleaseMutex bien qu'il ne semble pas vraiment nécessaire car il est publié automatiquement par le système d'exploitation (et si l'application se bloque et n'appelle jamais ReleaseMutex, auriez-vous besoin de redémarrer?).

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
la source