Mô hình tốt để sử dụng Mutex toàn cầu trong C # là gì?


377

Lớp Mutex rất dễ bị hiểu lầm và Global mutexes thậm chí còn hơn thế.

Mô hình tốt, an toàn để sử dụng khi tạo ra các đột biến toàn cầu là gì?

Một cái sẽ hoạt động

  • Bất kể miền địa phương máy của tôi đang ở
  • Được đảm bảo để phát hành mutex đúng cách
  • Tùy chọn không treo mãi mãi nếu không thu được mutex
  • Thỏa thuận với các trường hợp các quá trình khác từ bỏ mutex

Câu trả lời:


402

Tôi muốn chắc chắn rằng điều này ở ngoài kia, bởi vì thật khó để có được đúng:

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
bạn có thể muốn bỏ qua usingđể kiểm tra createdNewvà thêm mutex.Dispose()vào bên trong finally. Tôi không thể giải thích rõ ràng (Tôi không biết lý do) ngay bây giờ nhưng tôi đã có bản thân mình vào một tình huống khi mutex.WaitOnetrở lại truesau khi createdNewđã trở thành false(Tôi mua mutex trong hiện tại AppDomainvà sau đó nạp một mới AppDomainvà thực thi cùng mã từ trong đó).
Sergey.quixoticaxis.Ivanov

1. Có exitContext = falselàm gì trong mutex.WaitOne(5000, false)không? Có vẻ như nó chỉ có thể gây ra một khẳng định trong CoreCLR , 2. Nếu bất kỳ ai thắc mắc, trong nhà Mutexxây dựng, lý do tại sao initiallyOwnedđược falsegiải thích một phần bởi bài viết MSDN này .
jrh

3
Mẹo: coi chừng sử dụng Mutex với ASP.NET: "Lớp Mutex thực thi nhận dạng luồng, do đó, một mutex chỉ có thể được giải phóng bởi luồng đã thu được nó. Ngược lại, lớp Semaphore không thực thi nhận dạng luồng.". Một yêu cầu ASP.NET có thể được phục vụ bởi nhiều luồng.
Sam Ruither

sự kiện startupnextinstance an toàn trong VB.NET? không có trong C # docs.microsoft.com/es-es/dotnet/api/ Kẻ
Kiquenet

Xem câu trả lời của tôi mà không cần sử dụng WaitOne. stackoverflow.com/a/59079638/4491768
Wouter

129

Sử dụng câu trả lời được chấp nhận Tôi tạo một lớp trợ giúp để bạn có thể sử dụng nó theo cách tương tự như bạn sẽ sử dụng câu lệnh Khóa. Chỉ cần nghĩ rằng tôi muốn chia sẻ.

Sử dụng:

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

Và lớp người trợ giúp:

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

Tuyệt vời, cảm ơn! FYI: Tôi đã cập nhật phương pháp Vứt bỏ ở trên để ngăn cảnh báo CA2213 trong khi Phân tích mã. Phần còn lại trôi qua tốt đẹp. Để biết thêm chi tiết, hãy kiểm tra msdn.microsoft.com/query/ Mạnh
Pat Hermens

1
Làm cách nào để tôi xử lý ngoại lệ hết thời gian trong lớp tiêu thụ SingleGlobalInstance. Ngoài ra có phải là một thực hành tốt để ném ngoại lệ trong khi xây dựng một thể hiện?
kiran

3
Thời gian chờ bằng 0 vẫn là thời gian chờ bằng 0, không phải là vô cùng! Kiểm tra tốt hơn < 0thay vì <= 0.
ygoe

2
@antistar: Tôi thấy rằng sử dụng _mutex.Close()thay vì _mutex.Dispose()trong phương thức Vứt bỏ làm việc cho tôi. Lỗi là do cố gắng loại bỏ WaitHandle bên dưới. Mutex.Close()xử lý các tài nguyên cơ bản.
djpMusic

1
Nó cho thấy "AppName đã ngừng hoạt động." khi tôi cố gắng mở phiên bản thứ hai của ứng dụng. Tôi muốn đặt trọng tâm vào ứng dụng khi người dùng cố gắng mở phiên bản thứ hai của ứng dụng. Tôi làm nó như thế nào?
Bhaskar

13

Có một điều kiện cuộc đua trong câu trả lời được chấp nhận khi 2 quá trình chạy dưới 2 người dùng khác nhau cố gắng khởi tạo mutex cùng một lúc. Sau khi quy trình đầu tiên khởi tạo mutex, nếu quy trình thứ hai cố gắng khởi tạo mutex trước khi quy trình đầu tiên đặt quy tắc truy cập cho mọi người, một ngoại lệ trái phép sẽ bị ném bởi quy trình thứ hai.

Xem bên dưới để trả lời đúng:

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
Lưu ý vấn đề này hiện đã được khắc phục trong câu trả lời được chấp nhận.
Văn Nguyễn

10

Ví dụ này sẽ thoát sau 5 giây nếu một phiên bản khác đang chạy.

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

Cả Mutex và WinApi CreatMutex () đều không hoạt động với tôi.

Một giải pháp thay thế:

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

        Application.Run(new MainForm());

        SingleApplicationDetector.Close();
    }
}

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

Lý do sử dụng Semaphore thay vì Mutex:

Lớp Mutex thi hành danh tính luồng, do đó, một mutex chỉ có thể được giải phóng bởi luồng đã thu được nó. Ngược lại, lớp Semaphore không thực thi danh tính luồng.

<< System.Threading.Mutex

Tham chiếu: Semaphore.OpenEx hiện ()


7
Điều kiện cuộc đua có thể giữa Semaphore.OpenExistingnew Semaphore.
xmedeko

3

Đôi khi học bằng ví dụ giúp ích nhiều nhất. Chạy ứng dụng giao diện điều khiển này trong ba cửa sổ giao diện điều khiển khác nhau. Bạn sẽ thấy rằng ứng dụng bạn đã chạy đầu tiên mua lại mutex trước, trong khi hai ứng dụng khác đang chờ đến lượt. Sau đó nhấn enter trong ứng dụng đầu tiên, bạn sẽ thấy ứng dụng 2 hiện đang tiếp tục chạy bằng cách lấy mutex, tuy nhiên ứng dụng 3 đang chờ đến lượt. Sau khi bạn nhấn enter trong ứng dụng 2, bạn sẽ thấy ứng dụng 3 tiếp tục. Điều này minh họa khái niệm về một mutex bảo vệ một phần mã chỉ được thực thi bởi một luồng (trong trường hợp này là một quá trình) như viết vào một tệp làm ví dụ.

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

nhập mô tả hình ảnh ở đây


0

Mutex toàn cầu không chỉ đảm bảo chỉ có một phiên bản của ứng dụng. Cá nhân tôi thích sử dụng Microsoft.VisualBasic để đảm bảo ứng dụng đơn lẻ như được mô tả trong Cách chính xác để tạo ứng dụng WPF đơn lẻ là gì?(Câu trả lời của Dale Ragan) ... Tôi thấy việc chuyển các đối số nhận được khi khởi động ứng dụng mới sang ứng dụng cá thể ban đầu dễ dàng hơn.

Nhưng liên quan đến một số mã trước đó trong chuỗi này, tôi không muốn tạo Mutex mỗi lần tôi muốn có khóa. Nó có thể tốt cho một ứng dụng đơn lẻ nhưng trong cách sử dụng khác, nó dường như quá mức cần thiết.

Đó là lý do tại sao tôi đề nghị thực hiện thay thế:

Sử dụng:

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

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

    }
}

Một người bồi bàn

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

Một giải pháp (cho WPF) mà không có WaitOne vì nó có thể gây ra AbandiatedMutexException. Giải pháp này sử dụng hàm tạo Mutex trả về boolean đã tạo để kiểm tra xem mutex đã được tạo chưa. Nó cũng sử dụng GetType (). GUID để đổi tên một tệp thực thi không cho phép nhiều trường hợp.

Mutex toàn cầu và địa phương xem ghi chú trong: 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();
    }
}

Bởi vì Mutex thực hiện IDis Dùng một lần, nó được phát hành tự động nhưng để hoàn thành cuộc gọi, hãy loại bỏ:

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

Di chuyển mọi thứ vào một lớp cơ sở và thêm allowEveryoneRule từ câu trả lời được chấp nhận. Cũng đã thêm ReleaseMutex mặc dù có vẻ như nó không thực sự cần thiết bởi vì nó được hệ điều hành tự động phát hành (điều gì sẽ xảy ra nếu ứng dụng gặp sự cố và không bao giờ gọi ReleaseMutex bạn có cần khởi động lại không?).

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();
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.