Apa pola yang baik untuk menggunakan Global Mutex di C #?


377

Kelas Mutex sangat disalahpahami, dan Global mutex lebih dari itu.

Apa yang baik, pola aman untuk digunakan saat membuat Global mutexes?

Yang akan bekerja

  • Terlepas dari lokal tempat mesin saya berada
  • Dijamin untuk melepaskan mutex dengan benar
  • Secara opsional tidak menggantung selamanya jika mutex tidak diperoleh
  • Berurusan dengan kasus di mana proses lain meninggalkan mutex

Jawaban:


402

Saya ingin memastikan ini ada di luar sana, karena sangat sulit untuk memperbaikinya:

using System.Runtime.InteropServices;   //GuidAttribute
using System.Reflection;                //Assembly
using System.Threading;                 //Mutex
using System.Security.AccessControl;    //MutexAccessRule
using System.Security.Principal;        //SecurityIdentifier

static void Main(string[] args)
{
    // get application GUID as defined in AssemblyInfo.cs
    string appGuid =
        ((GuidAttribute)Assembly.GetExecutingAssembly().
            GetCustomAttributes(typeof(GuidAttribute), false).
                GetValue(0)).Value.ToString();

    // unique id for global mutex - Global prefix means it is global to the machine
    string mutexId = string.Format( "Global\\{{{0}}}", appGuid );

    // Need a place to store a return value in Mutex() constructor call
    bool createdNew;

    // edited by Jeremy Wiebe to add example of setting up security for multi-user usage
    // edited by 'Marc' to work also on localized systems (don't use just "Everyone") 
    var allowEveryoneRule =
        new MutexAccessRule( new SecurityIdentifier( WellKnownSidType.WorldSid
                                                   , null)
                           , MutexRights.FullControl
                           , AccessControlType.Allow
                           );
    var securitySettings = new MutexSecurity();
    securitySettings.AddAccessRule(allowEveryoneRule);

   // edited by MasonGZhwiti to prevent race condition on security settings via VanNguyen
    using (var mutex = new Mutex(false, mutexId, out createdNew, securitySettings))
    {
        // edited by acidzombie24
        var hasHandle = false;
        try
        {
            try
            {
                // note, you may want to time out here instead of waiting forever
                // edited by acidzombie24
                // mutex.WaitOne(Timeout.Infinite, false);
                hasHandle = mutex.WaitOne(5000, false);
                if (hasHandle == false)
                    throw new TimeoutException("Timeout waiting for exclusive access");
            }
            catch (AbandonedMutexException)
            {
                // Log the fact that the mutex was abandoned in another process,
                // it will still get acquired
                hasHandle = true;
            }

            // Perform your work here.
        }
        finally
        {
            // edited by acidzombie24, added if statement
            if(hasHandle)
                mutex.ReleaseMutex();
        }
    }
}

1
Anda mungkin ingin menghilangkan usinguntuk memeriksa createdNewdan menambahkan mutex.Dispose()di dalam finally. Saya tidak dapat menjelaskannya dengan jelas (saya tidak tahu alasannya) saat ini tetapi saya sendiri mengalami situasi ketika mutex.WaitOnekembali truesetelah createdNewmenjadi false(saya memperoleh mutex di arus AppDomaindan kemudian memuat yang baru AppDomaindan mengeksekusi kode yang sama dari didalamnya).
Sergey.quixoticaxis.Ivanov

1. Apakah exitContext = falsemelakukan sesuatu mutex.WaitOne(5000, false)? Kelihatannya seperti itu hanya bisa menyebabkan menegaskan di CoreCLR , 2. Jika siapa pun bertanya-tanya, di Mutex's konstruktor, alasan mengapa initiallyOwnedadalah falsesebagian dijelaskan oleh artikel MSDN ini .
jrh

3
Kiat: berhati-hatilah menggunakan Mutex dengan ASP.NET: "Kelas Mutex memberlakukan identitas utas, sehingga mutex hanya dapat dirilis oleh utas yang mendapatkannya. Sebaliknya, kelas Semaphore tidak menegakkan identitas utas.". Permintaan ASP.NET dapat dilayani oleh beberapa utas.
Sam Rueby

acara startupnextinstance aman di VB.NET? tidak ada dalam C # docs.microsoft.com/es-es/dotnet/api/…
Kiquenet

Lihat jawaban saya tanpa menggunakan WaitOne. stackoverflow.com/a/59079638/4491768
Wouter

129

Menggunakan jawaban yang diterima saya membuat kelas penolong sehingga Anda bisa menggunakannya dengan cara yang sama Anda akan menggunakan pernyataan Lock. Saya pikir saya akan berbagi.

Menggunakan:

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

Dan kelas pembantu:

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

Kerja luar biasa, terima kasih! FYI: Saya telah memperbarui metode Buang di atas untuk mencegah peringatan CA2213 selama Analisis Kode. Sisanya berlalu dengan baik. Untuk detail lebih lanjut, periksa msdn.microsoft.com/query/…
Pat Hermens

1
Bagaimana cara menangani pengecualian batas waktu di kelas yang mengkonsumsi SingleGlobalInstance. Juga apakah itu praktik yang baik untuk membuang pengecualian saat membangun sebuah instance?
kiran

3
Batas waktu 0 tetap harus batas waktu nol, bukan tak terbatas! Lebih baik periksa < 0daripada <= 0.
ygoe

2
@ Antistar: Saya menemukan bahwa menggunakan _mutex.Close()alih-alih _mutex.Dispose()dalam metode Buang bekerja untuk saya. Kesalahan itu disebabkan oleh mencoba untuk membuang WaitHandle yang mendasarinya. Mutex.Close()membuang sumber daya yang mendasarinya.
djpMusic

1
Itu menunjukkan "AppName telah berhenti bekerja." ketika saya mencoba untuk membuka instance aplikasi kedua. Saya ingin mengatur fokus pada aplikasi ketika pengguna mencoba membuka instance kedua dari aplikasi. Bagaimana saya bisa melakukannya?
Bhaskar

13

Ada kondisi ras dalam jawaban yang diterima ketika 2 proses berjalan di bawah 2 pengguna yang berbeda yang mencoba menginisialisasi mutex pada saat yang sama. Setelah proses pertama menginisialisasi mutex, jika proses kedua mencoba untuk menginisialisasi mutex sebelum proses pertama menetapkan aturan akses untuk semua orang, pengecualian yang tidak sah akan dilemparkan oleh proses kedua.

Lihat di bawah untuk jawaban yang benar:

using System.Runtime.InteropServices;   //GuidAttribute
using System.Reflection;                //Assembly
using System.Threading;                 //Mutex
using System.Security.AccessControl;    //MutexAccessRule
using System.Security.Principal;        //SecurityIdentifier

static void Main(string[] args)
{
    // get application GUID as defined in AssemblyInfo.cs
    string appGuid = ((GuidAttribute)Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(GuidAttribute), false).GetValue(0)).Value.ToString();

    // unique id for global mutex - Global prefix means it is global to the machine
    string mutexId = string.Format( "Global\\{{{0}}}", appGuid );

    bool createdNew;
        // edited by Jeremy Wiebe to add example of setting up security for multi-user usage
        // edited by 'Marc' to work also on localized systems (don't use just "Everyone") 
        var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
        var securitySettings = new MutexSecurity();
        securitySettings.AddAccessRule(allowEveryoneRule);

        using (var mutex = new Mutex(false, mutexId, out createdNew, securitySettings))
        {

        // edited by acidzombie24
        var hasHandle = false;
        try
        {
            try
            {
                // note, you may want to time out here instead of waiting forever
                // edited by acidzombie24
                // mutex.WaitOne(Timeout.Infinite, false);
                hasHandle = mutex.WaitOne(5000, false);
                if (hasHandle == false)
                    throw new TimeoutException("Timeout waiting for exclusive access");
            }
            catch (AbandonedMutexException)
            {
                // Log the fact the mutex was abandoned in another process, it will still get aquired
                hasHandle = true;
            }

            // Perform your work here.
        }
        finally
        {
            // edited by acidzombie24, added if statemnet
            if(hasHandle)
                mutex.ReleaseMutex();
        }
    }
}

8
Perhatikan masalah ini sekarang diperbaiki dalam jawaban yang diterima.
Van Nguyen

10

Contoh ini akan keluar setelah 5 detik jika instance lain sudah berjalan.

// unique id for global mutex - Global prefix means it is global to the machine
const string mutex_id = "Global\\{B1E7934A-F688-417f-8FCB-65C3985E9E27}";

static void Main(string[] args)
{

    using (var mutex = new Mutex(false, mutex_id))
    {
        try
        {
            try
            {
                if (!mutex.WaitOne(TimeSpan.FromSeconds(5), false))
                {
                    Console.WriteLine("Another instance of this program is running");
                    Environment.Exit(0);
                }
            }
            catch (AbandonedMutexException)
            {
                // Log the fact the mutex was abandoned in another process, it will still get aquired
            }

            // Perform your work here.
        }
        finally
        {
            mutex.ReleaseMutex();
        }
    }
}

10

Baik Mutex maupun WinApi CreateMutex () tidak berfungsi untuk saya.

Solusi alternatif:

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

        Application.Run(new MainForm());

        SingleApplicationDetector.Close();
    }
}

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

Alasan menggunakan Semaphore, bukan Mutex:

Kelas Mutex memberlakukan identitas utas, sehingga mutex hanya dapat dirilis oleh utas yang mendapatkannya. Sebaliknya, kelas Semaphore tidak menegakkan identitas utas.

<< System.Threading.Mutex

Ref: Semaphore.OpenExisting ()


7
Kemungkinan kondisi balapan antara Semaphore.OpenExistingdan new Semaphore.
xmedeko

3

Terkadang belajar dengan memberi contoh sangat membantu. Jalankan aplikasi konsol ini di tiga jendela konsol yang berbeda. Anda akan melihat bahwa aplikasi yang Anda jalankan pertama mendapatkan mutex terlebih dahulu, sementara dua lainnya menunggu giliran. Kemudian tekan enter di aplikasi pertama, Anda akan melihat bahwa aplikasi 2 sekarang terus berjalan dengan memperoleh mutex, namun aplikasi 3 sedang menunggu giliran. Setelah Anda menekan enter dalam aplikasi 2 Anda akan melihat bahwa aplikasi 3 berlanjut. Ini menggambarkan konsep mutex yang melindungi bagian kode yang akan dieksekusi hanya oleh satu utas (dalam hal ini proses) seperti menulis ke file sebagai contoh.

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

masukkan deskripsi gambar di sini


0

Mutex global tidak hanya memastikan memiliki hanya satu instance aplikasi. Saya pribadi lebih suka menggunakan Microsoft.VisualBasic untuk memastikan aplikasi instance tunggal seperti dijelaskan dalam Apa cara yang benar untuk membuat aplikasi WPF instance tunggal? (Jawaban Dale Ragan) ... Saya menemukan bahwa lebih mudah untuk menyampaikan argumen yang diterima pada startup aplikasi baru ke aplikasi instance tunggal awal.

Tetapi mengenai beberapa kode sebelumnya di utas ini, saya lebih suka untuk tidak membuat Mutex setiap kali saya ingin memiliki kunci di dalamnya. Itu bisa saja baik untuk aplikasi contoh tunggal tetapi dalam penggunaan lain tampaknya bagi saya telah berlebihan.

Itu sebabnya saya menyarankan implementasi ini sebagai gantinya:

Pemakaian:

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

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

    }
}

Pelayan

using System;

namespace HQ.Util.General.Threading
{
    public class MutexGlobalAwaiter : IDisposable
    {
        MutexGlobal _mutexGlobal = null;

        public bool HasTimedOut { get; set; } = false;

        internal MutexGlobalAwaiter(MutexGlobal mutexEx, int timeOut)
        {
            _mutexGlobal = mutexEx;

            do
            {
                HasTimedOut = !_mutexGlobal.Mutex.WaitOne(timeOut, false);
                if (! HasTimedOut) // Signal received
                {
                    return;
                }
            } while (_mutexGlobal.FuncTimeOutRetry(timeOut));
        }

        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    _mutexGlobal.Mutex.ReleaseMutex();
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                disposedValue = true;
            }
        }
        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        // ~MutexExAwaiter()
        // {
        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        //   Dispose(false);
        // }

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            // TODO: uncomment the following line if the finalizer is overridden above.
            // GC.SuppressFinalize(this);
        }
        #endregion
    }
}

0

Solusi (untuk WPF) tanpa WaitOne karena dapat menyebabkan AbandonedMutexException. Solusi ini menggunakan konstruktor Mutex yang mengembalikan boolean CreatedNew untuk memeriksa apakah mutex sudah dibuat. Itu juga menggunakan GetType (). GUID jadi mengubah nama sebuah executable tidak memungkinkan beberapa instance.

Global vs mutex lokal lihat catatan di: 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();
    }
}

Karena Mutex mengimplementasikan IDisposable, itu dirilis secara otomatis tetapi untuk panggilan kelengkapan, buang:

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

Pindahkan semuanya ke kelas dasar dan tambahkan allowEveryoneRule dari jawaban yang diterima. Juga menambahkan ReleaseMutex meskipun sepertinya tidak benar-benar diperlukan karena dirilis secara otomatis oleh OS (bagaimana jika aplikasi crash dan tidak pernah memanggil ReleaseMutex, Anda perlu reboot?).

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();
        }
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.