รูปแบบที่ดีสำหรับการใช้ Global Mutex ใน C # คืออะไร


377

คลาส Mutex เข้าใจผิดมากและ Mutex ทั่วโลกยิ่งกว่านั้นมาก

รูปแบบที่ดีและปลอดภัยที่จะใช้เมื่อสร้าง Global mutexes คืออะไร

หนึ่งที่จะทำงาน

  • เครื่องของฉันอยู่ในระบบ
  • รับประกันว่าจะปล่อย mutex อย่างถูกต้อง
  • เป็นทางเลือกไม่แขวนตลอดไปหากไม่ได้รับ mutex
  • ข้อตกลงกับกรณีที่กระบวนการอื่นละทิ้ง mutex

คำตอบ:


402

ฉันต้องการให้แน่ใจว่าสิ่งนี้อยู่ที่นั่นเพราะมันยากที่จะพูดถูก:

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
คุณอาจต้องการที่จะละเว้นusingการตรวจสอบcreatedNewและเพิ่มภายในmutex.Dispose() finallyฉันไม่สามารถอธิบายได้อย่างชัดเจน (ผมไม่ทราบเหตุผล) แต่ตอนนี้ฉันมีตัวเองให้สถานการณ์เมื่อmutex.WaitOneกลับมาtrueหลังจากที่createdNewกลายเป็นfalse(ผมได้มา mutex ในปัจจุบันAppDomainแล้วโหลดใหม่AppDomainและดำเนินการรหัสเดียวกันจาก อยู่ภายใน).
Sergey.quixoticaxis.Ivanov

1. ไม่exitContext = falseทำอะไรmutex.WaitOne(5000, false)? ดูเหมือนว่ามันจะทำให้เกิดการยืนยันใน CoreCLR , 2. หากใครสงสัยในMutex's constructor เหตุผลที่initiallyOwnedจะfalseมีการอธิบายบางส่วนจากบทความนี้ MSDN
jrh

3
คำแนะนำ: ระวังการใช้ Mutex กับ ASP.NET: "คลาส Mutex บังคับใช้เธรดเพื่อให้ mutex สามารถปล่อยได้โดยเธรดที่ได้รับเท่านั้นโดยทางตรงกันข้ามคลาสเซมาฟอร์ไม่บังคับใช้ข้อมูลเฉพาะตัวของเธรด" คำร้องขอ ASP.NET สามารถให้บริการได้โดยหลายเธรด
Sam Rueby

เหตุการณ์startupnextinstanceอย่างปลอดภัยใน VB.NET? ไม่อยู่ใน C # docs.microsoft.com/es-es/dotnet/api/…
Kiquenet

ดูคำตอบของฉันโดยไม่ใช้ WaitOne stackoverflow.com/a/59079638/4491768
Wouter

129

การใช้คำตอบที่ได้รับการยอมรับฉันสร้างคลาสผู้ช่วยเพื่อให้คุณสามารถใช้มันในลักษณะเดียวกันกับที่คุณจะใช้คำสั่งล็อค แค่คิดว่าฉันจะแบ่งปัน

ใช้:

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

และชั้นผู้ช่วย:

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

ผลงานยอดเยี่ยมขอบคุณ! FYI: ฉันได้อัปเดตวิธีการกำจัดข้างต้นเพื่อป้องกันคำเตือน CA2213 ระหว่างการวิเคราะห์รหัส ส่วนที่เหลือผ่านไปได้ดี สำหรับรายละเอียดเพิ่มเติมโปรดตรวจสอบmsdn.microsoft.com/query/ ......
Pat

1
ฉันจะจัดการกับข้อยกเว้นการหมดเวลาในคลาสที่ใช้งาน SingleGlobalInstance ได้อย่างไร นอกจากนี้ยังเป็นวิธีที่ดีที่จะโยนข้อยกเว้นในขณะที่สร้างอินสแตนซ์?
kiran

3
การหมดเวลาของ 0 ควรยังคงเป็นการหมดเวลาของศูนย์ไม่ใช่อินฟินิตี้! การตรวจสอบที่ดีกว่าสำหรับแทน< 0 <= 0
ygoe

2
@antistar: ฉันพบว่าการใช้_mutex.Close()แทน_mutex.Dispose()วิธี Dispose นั้นใช้ได้กับฉัน ข้อผิดพลาดเกิดจากการพยายามกำจัด WaitHandle ที่อยู่ภายใต้ Mutex.Close()การทิ้งทรัพยากรพื้นฐาน
djpMusic

1
แสดงว่า "AppName หยุดทำงาน" เมื่อฉันพยายามเปิดแอปอินสแตนซ์ที่สอง ฉันต้องการตั้งค่าโฟกัสบนแอพเมื่อผู้ใช้พยายามเปิดอินสแตนซ์ที่สองของแอพ ฉันจะทำมันได้อย่างไร
Bhaskar

13

มีเงื่อนไขการแย่งชิงในคำตอบที่ยอมรับเมื่อ 2 กระบวนการทำงานภายใต้ 2 ผู้ใช้ที่ต่างกันพยายามเริ่มต้น mutex ในเวลาเดียวกัน หลังจากกระบวนการแรกเริ่มต้น mutex หากกระบวนการที่สองพยายามเริ่มต้น mutex ก่อนที่กระบวนการแรกจะตั้งค่ากฎการเข้าถึงให้ทุกคนข้อยกเว้นที่ไม่ได้รับอนุญาตจะถูกโยนโดยกระบวนการที่สอง

ดูคำตอบที่ถูกต้องด้านล่าง:

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
หมายเหตุปัญหานี้ได้รับการแก้ไขแล้วในคำตอบที่ยอมรับแล้ว
Van Nguyen

10

ตัวอย่างนี้จะออกหลังจาก 5 วินาทีหากอินสแตนซ์อื่นกำลังทำงานอยู่

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

Mutex และ WinApi CreateMutex () ไม่เหมาะกับฉัน

ทางเลือกอื่น:

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

เหตุผลที่ใช้เซมาฟอร์แทน Mutex:

คลาส Mutex บังคับใช้เอกลักษณ์ของเธรดดังนั้น mutex สามารถถูกปล่อยได้โดยเธรดที่รับมาเท่านั้น ในทางตรงกันข้ามคลาสเซมาฟอร์ไม่ได้บังคับใช้เอกลักษณ์ของเธรด

<< ระบบ. เธเธเธเธเธฒเธ

อ้างอิง: สัญญาณเปิดที่มีอยู่ ()


7
เงื่อนไขการแข่งขันที่เป็นไปได้ระหว่างและSemaphore.OpenExisting new Semaphore
xmedeko

3

บางครั้งการเรียนรู้จากตัวอย่างช่วยให้มากที่สุด เรียกใช้แอปพลิเคชันคอนโซลนี้ในหน้าต่างคอนโซลสามหน้าต่างที่แตกต่างกัน คุณจะเห็นว่าแอปพลิเคชันที่คุณรันก่อนได้รับ mutex ก่อนในขณะที่อีกสองรายการกำลังรอการเปิดใช้งาน จากนั้นกด Enter ในแอปพลิเคชันแรกคุณจะเห็นว่าแอปพลิเคชัน 2 ทำงานต่อไปโดยรับ Mutex อย่างไรก็ตามแอปพลิเคชัน 3 กำลังรอการเปิดใช้งาน หลังจากที่คุณกด Enter ในแอปพลิเคชัน 2 คุณจะเห็นว่าแอปพลิเคชัน 3 ดำเนินการต่อ นี่แสดงให้เห็นถึงแนวคิดของ mutex ที่ปกป้องส่วนของรหัสที่จะดำเนินการโดยหนึ่งเธรด (ในกรณีนี้กระบวนการ) เช่นการเขียนไปยังไฟล์เป็นตัวอย่าง

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

ป้อนคำอธิบายรูปภาพที่นี่


0

Mutex ทั่วโลกไม่เพียง แต่เพื่อให้แน่ใจว่ามีอินสแตนซ์ของแอปพลิเคชันเดียวเท่านั้น ฉันชอบใช้ Microsoft.VisualBasic เป็นการส่วนตัวเพื่อให้แน่ใจว่าแอปพลิเคชันอินสแตนซ์เดียวเช่นที่อธิบายไว้ในวิธีที่ถูกต้องในการสร้างแอปพลิเคชัน WPF อินสแตนซ์เดียวคืออะไร (คำตอบของ Dale Ragan) ... ฉันพบว่าง่ายกว่าที่จะส่งผ่านข้อโต้แย้งที่ได้รับจากการเริ่มต้นแอปพลิเคชันใหม่ไปยังแอปพลิเคชันอินสแตนซ์เดี่ยวเริ่มต้น

แต่เกี่ยวกับรหัสก่อนหน้าบางส่วนในชุดนี้ฉันไม่ต้องการสร้าง Mutex ทุกครั้งที่ฉันต้องการล็อคมัน มันอาจจะดีสำหรับแอปพลิเคชันอินสแตนซ์เดียว แต่ในการใช้งานอื่น ๆ ดูเหมือนว่าฉันจะ overkill

นั่นเป็นเหตุผลที่ฉันแนะนำการใช้งานนี้แทน:

การใช้งาน:

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

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

    }
}

บริกร

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

โซลูชัน (สำหรับ WPF) โดยไม่มี WaitOne เนื่องจากสามารถทำให้เกิด AbandonedMutexException วิธีการแก้ปัญหานี้ใช้ตัวสร้าง Mutex ที่ส่งกลับบูลีน createdNew เพื่อตรวจสอบว่า Mutex ถูกสร้างขึ้นแล้ว นอกจากนี้ยังใช้ GetType () อีกด้วย GUID ดังนั้นการเปลี่ยนชื่อไฟล์ปฏิบัติการไม่อนุญาตให้มีหลายอินสแตนซ์

Mutex ท้องถิ่นเทียบกับ Local ดูหมายเหตุใน: 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();
    }
}

เนื่องจาก Mutex ใช้ IDisposable มันจะถูกปล่อยออกมาโดยอัตโนมัติ แต่สำหรับการโทรโดยสมบูรณ์:

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

ย้ายทุกอย่างลงในคลาสฐานและเพิ่ม allowEveryoneRule จากคำตอบที่ยอมรับ เพิ่ม ReleaseMutex แม้ว่ามันจะดูไม่เป็นที่ต้องการจริงๆเพราะมันถูกปล่อยออกมาโดยอัตโนมัติโดยระบบปฏิบัติการ (จะเกิดอะไรขึ้นถ้าแอปพลิเคชันขัดข้องและไม่เคยโทร ReleaseMutex คุณจะต้องรีบูตหรือไม่)

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();
        }
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.