線程同步 –AutoResetEvent和ManualResetEvent


上一篇介紹了通過lock關鍵字和Monitor類型進行線程同步,本篇中就介紹一下通過同步句柄進行線程同步。

在Windows系統中,可以使用內核對象進行線程同步,內核對象由系統創建並維護。內核對象為內核所擁有,所以不同進程可以訪問同一個內核對象, 如進程、線程、事件、信號量、互斥量等都是內核對象。其中,信號量,互斥體,事件是Windows專門用來進行線程同步的內核對象。

在.NET中,有一個WaitHandle抽象類,這個類型封裝了一個Windows內核對象句柄,在C#代碼中,我們就可以使用WaitHandle類型(確切的說是子類型)的實例進行線程同步了。下面圖中顯示了WaitHandle類型的所有子類,下面對各個類型進行介紹。

WaitHandle類型

WaitHandle類型中的SafeWaitHandle屬性就是我們前面提到的Windows內核對象句柄,WaitHandle是一個抽象類,不能實例化。

下面看看WaitHandle中常用方法:

  • 實例方法:
    • WaitOne():阻止當前線程,直到當前 WaitHandle 收到信號
    • WaitOne(Int32):阻止當前線程,直到當前 WaitHandle 收到信號,同時使用 32 位帶符號整數表示超時時間
  • 靜態方法:
    • WaitAll(WaitHandle[]):等待指定數組中的所有元素都收到信號
    • WaitAll(WaitHandle[], Int32):等待指定數組中的所有元素接收信號,同時使用 32 位帶符號整數表示超時時間
    • WaitAny(WaitHandle[]):等待指定數組中的任一元素收到信號
    • WaitAny(WaitHandle[], Int32):等待指定數組中的任意元素接收信號,同時使用 32 位帶符號整數表示超時時間

同步事件EventWaitHandle

EventWaitHandle 類允許線程通過發信號互相通信, 通常情況下,一個或多個線程在 EventWaitHandle 上阻止,直到一個未阻止的線程調用 Set 方法,以釋放一個或多個被阻止的線程。

在EventWaitHandle類型中,除了父類中的方法,又有自己的特有方法,下面幾個是比較常用的:

  • 實例方法:
    • Set:將事件狀態設置為終止狀態,允許一個或多個等待線程繼續
    • Reset:將事件狀態設置為非終止狀態,導致線程阻止
  • 靜態方法:
    • OpenExisting(String):打開指定名稱為同步事件(如果已經存在);通過一個命名的EventWaitHandle我們可以進行進程之間的線程同步,未命名的EventWaitHandle只能進行本進程中的線程同步

EventWaitHandle類型有兩個子類AutoResetEvent和ManualResetEvent,這兩個子類分別代表了EventWaitHandle類型對事件狀態的重置模式。在釋放單個等待線程后,用 EventResetMode.AutoReset 標志創建的 EventWaitHandle 在終止時會自動重置; 用 EventResetMode.ManualReset 標志創建的 EventWaitHandle 一直保持終止狀態,直到它的 Reset 方法被調用

通過EventWaitHandle類型的構造函數,我們可以通過參數指定EventResetMode,從而選擇事件狀態的重置模式。當然,我們也可以直接使用EventWaitHandle的兩個子類。

public EventWaitHandle(bool initialState, EventResetMode mode);

public enum EventResetMode
{
    AutoReset = 0,
    ManualReset = 1,
}

使用AutoResetEvent

AutoResetEvent 允許線程通過發信號互相通信:

  • 線程通過調用 AutoResetEvent 上的 WaitOne 來等待信號。 如果 AutoResetEvent 為非終止狀態,則線程會被阻止,並等待當前控制資源的線程通過調用 Set 來通知資源可用。
  • 線程通過調用 Set 向 AutoResetEvent 發信號以釋放等待線程。 AutoResetEvent 將保持終止狀態,直到一個正在等待的線程被釋放,然后自動返回非終止狀態。 如果沒有任何線程在等待,則狀態將無限期地保持為終止狀態。

可以通過將一個布爾值傳遞給構造函數來控制 AutoResetEvent 的初始狀態:如果初始狀態為終止狀態,則為 true;否則為 false。

下面看一個例子:

namespace AutoResetEventTest
{
    class Program
    {
        //AutoResetEvent實例初始為非終止狀態
        private static AutoResetEvent autoResetEvent = new AutoResetEvent(false);

        static void Main(string[] args)
        {
            new Thread(() =>
            {
                while (true)
                {
                    //調用WaitOne來等待信號,並設置超時時間為5秒
                    bool status = autoResetEvent.WaitOne(5000);
                    if (status)
                    {
                        Console.WriteLine("ThreadOne get the signal");
                    }
                    else
                    {
                        Console.WriteLine("ThreadOne timeout(5 seconds) waiting for signal");
                        break;
                    }
                }
                Console.WriteLine("ThreadOne Exit");

            }).Start();

            new Thread(() =>
            {
                while (true)
                {
                    //調用WaitOne來等待信號,並設置超時時間為5秒
                    bool status = autoResetEvent.WaitOne(5000);
                    if (status)
                    {
                        Console.WriteLine("ThreadTwo get the signal");
                    }
                    else
                    {
                        Console.WriteLine("ThreadTwo timeout(5 seconds) waiting for signal");
                        break;
                    }
                }
                Console.WriteLine("ThreadTwo Exit");

            }).Start();

            Random ran = new Random();
            for (int i = 0; i < 8; i++)
            {
                
                Thread.Sleep(ran.Next(500, 1000));
                //通過Set向 AutoResetEvent 發信號以釋放等待線程
                Console.WriteLine("Main thread send the signal");
                autoResetEvent.Set();
            }

            Console.Read();
        }
    }
}

代碼的輸出為下,通過結果也可以驗證,每次調用Set方法之后,AutoResetEvent 將保持終止狀態,直到一個正在等待的線程被釋放,然后自動返回非終止狀態。

使用ManualResetEvent

像AutoResetEvent一樣,ManualResetEvent 也是線程通過發信號互相通信:

  • 線程通過調用 ManualResetEvent上的 WaitOne 來等待信號。 如果 ManualResetEvent為非終止狀態,則線程會被阻止,並等待當前控制資源的線程通過調用 Set 來通知資源可用。
  • 線程通過調用 Set 向 ManualResetEvent發信號以釋放等待線程。 與AutoResetEvent不同的是,ManualResetEvent將一直保持終止狀態,並釋放所有等待線程,直到有線程通過Reset將 ManualResetEvent 置於非終止狀態。
  • 線程通過調用 Reset以將 ManualResetEvent 置於非終止狀態。

可以通過將布爾值傳遞給構造函數來控制 ManualResetEvent 的初始狀態,如果初始狀態處於終止狀態,為 true;否則為 false。

看一個例子:

namespace ManualResetEventTest
{
    class Program
    {
        //ManualResetEvent實例初始為非終止狀態
        private static ManualResetEvent manualResetEvent = new ManualResetEvent(false);

        static void Main(string[] args)
        {
            new Thread(() =>
            {
                //調用WaitOne來等待信號
                manualResetEvent.WaitOne();
                Console.WriteLine("Thread get the signal - the first time");

                Thread.Sleep(1000);
                manualResetEvent.WaitOne();
                Console.WriteLine("Thread get the signal - the second time");

                //調用Reset來以將 ManualResetEvent 置於非終止狀態
                Console.WriteLine("Child thread reset ManualResetEvent to non-signaled");
                manualResetEvent.Reset();

                manualResetEvent.WaitOne();
                Console.WriteLine("Thread get the signal - the third time");

                Console.WriteLine("Child thread reset ManualResetEvent to non-signaled");
                manualResetEvent.Reset();

                //調用WaitOne來等待信號,並設置超時時間為3秒
                manualResetEvent.WaitOne(3000);
                Console.WriteLine("timeout while waiting for signal");
            }).Start();

            //通過Set向 ManualResetEvent 發信號以釋放等待線程
            Console.WriteLine("Main thread set ManualResetEvent to signaled");
            manualResetEvent.Set();
            Thread.Sleep(3000);
            Console.WriteLine("Main thread set ManualResetEvent to signaled");
            manualResetEvent.Set();

            Console.Read();
        }
    }
}

代碼的輸出如下,通過結果驗證了,每次調用Set都會將ManualResetEvent設置為終止狀態,並釋放所有等待線程。只有手動調用 Reset才能將 ManualResetEvent 置於非終止狀態。

實現進程間線程同步

前一篇文章中介紹的lock和Monitor只能進行同一個進程中的線程同步。

但是,由於同步事件EventWaitHandle是基於內核事件的,所以說,它可以實現進程之間的線程同步。

基於前面AutoResetEvent的例子稍作修改:

class Program
{
    
    private static EventWaitHandle eventWaitHandle;

    private static bool newEventWaitHandleObj = true;

    static void Main(string[] args)
    {
        string EventWaitHandleName = "EventWaitHandleTest";
        try
        {
            //嘗試打開已有的同步事件
            eventWaitHandle = EventWaitHandle.OpenExisting("EventWaitHandleTest");
            newEventWaitHandleObj = false;
        }
        catch (WaitHandleCannotBeOpenedException e)
        {
            Console.WriteLine("EventWaitHandle named {0} is not exist, error message: {1}", EventWaitHandleName, e.Message);
            //實例化同步事件,初始為非終止狀態,設置為自動重置模式
            eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, "EventWaitHandleTest");
            Console.WriteLine("Create EventWaitHandle {0}", EventWaitHandleName);
            newEventWaitHandleObj = true;
        }
       
        new Thread(() =>
        {
            while (true)
            {
                //調用WaitOne來等待信號,並設置超時時間為5秒
                bool status = eventWaitHandle.WaitOne(5000);
                if (status)
                {
                    Console.WriteLine("ThreadOne get the signal");
                }
                else
                {
                    Console.WriteLine("ThreadOne timeout(5 seconds) waiting for signal");
                    break;
                }
            }
            Console.WriteLine("ThreadOne Exit");

        }).Start();

        new Thread(() =>
        {
            while (true)
            {
                //調用WaitOne來等待信號,並設置超時時間為5秒
                bool status = eventWaitHandle.WaitOne(5000);
                if (status)
                {
                    Console.WriteLine("ThreadTwo get the signal");
                }
                else
                {
                    Console.WriteLine("ThreadTwo timeout(5 seconds) waiting for signal");
                    break;
                }
            }
            Console.WriteLine("ThreadTwo Exit");

        }).Start();

        if (newEventWaitHandleObj)
        {
            Random ran = new Random();
            for (int i = 0; i < 8; i++)
            {

                Thread.Sleep(ran.Next(500, 1000));
                //通過Set向 AutoResetEvent 發信號以釋放等待線程
                Console.WriteLine("Main thread send the signal");
                eventWaitHandle.Set();
            }
        }

        Console.Read();
    }
}

代碼的輸出為下,代碼中通過OpenExisting方法嘗試打開已存在的同步事件句柄,如果失敗,就創建一個EventWaitHandle實例。

至於后面部分代碼的工作原理,跟AutoResetEvent的例子完全一樣。

接下來,我們找到工程生成的exe文件,然后同時啟動兩次exe文件,可以看到如下輸出,后面啟動的進程能夠打開前面進程創建的同步事件句柄。通過這種方式,就可以實現進程之間的線程同步。

總結

本文介紹了WaitHandle類型,以及該類型的子類型EventWaitHandle,並且介紹了如何通過AutoResetEvent和ManualResetEvent進行線程同步。

AutoResetEvent和ManualResetEvent的區別:

  • AutoResetEvent.WaitOne()每次只允許一個線程進入,當某個線程得到信號后,AutoResetEvent會自動又將信號置為非終止狀態,則其他調用WaitOne的線程只有繼續等待,也就是說AutoResetEvent一次只喚醒一個線程
  • ManualResetEvent則可以喚醒多個線程,因為當某個線程調用了ManualResetEvent.Set()方法后,其他調用WaitOne的線程獲得信號得以繼續執行,而ManualResetEvent不會自動將信號置為非終止狀態。也就是說,除非手工調用了ManualResetEvent.Reset()方法,則ManualResetEvent將一直保持有信號狀態,ManualResetEvent也就可以同時喚醒多個線程繼續執行。

下一篇將繼續介紹互斥體Mutex和信號量Semaphore的使用。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM