關於c#多執行緒中的幾個訊號量

阿文不知所措發表於2022-05-29

訊號量在c#多執行緒通訊中主要用來向阻塞的執行緒傳達訊號從而使得阻塞執行緒繼續執行

多執行緒訊號(執行緒互動):通常是指執行緒必須等待一個執行緒或者多個執行緒通知互動(釋放訊號)才可以繼續執行
在c#中訊號量主要有這幾個 AutoResetEvent,ManualResetEvent,CountdownEvent,EventWaitHandle,Semaphore

AutoResetEvent

AutoResetEvent 在釋放訊號量後,會預設設定為無訊號狀態。AutoResetEvent 建構函式會傳遞一個initialState boolean 型別的引數,引數為false 時 需要主動去傳遞信
號量,傳遞訊號量之後將重新設定為無訊號狀態。引數為ture 時會自動設定為有訊號狀態(終止狀態),大體意思就是,會預設執行阻塞執行緒,不需要阻塞執行緒收到訊號量才會執
行(不會阻塞呼叫執行緒)。在引數為ture 時,AutoResetEvent 類例項呼叫 Reset () 方法後,會將當前AutoResetEvent 類例項設定為無訊號狀態也就是 變成了一個 引數為
false 的 AutoResetEvent 類例項,在此之後的執行阻塞執行緒都需要主動去釋放(傳遞)訊號。

private static AutoResetEvent auto = new AutoResetEvent(false);
private static AutoResetEvent auto = new AutoResetEvent(ture);//有訊號終止狀態
Thread thread1 = new Thread(AutoResetEventHandler);
            Console.WriteLine("當前執行緒id"+Thread.CurrentThread.ManagedThreadId);
            thread1.Start();
            Thread.Sleep(5000);
            auto.Set();
           // auto.Reset();  在這種情況下new AutoResetEvent(ture) 的 類例項 會變成無訊號未終止狀態的 如果阻塞執行緒沒有接收到訊號量將會一直阻塞下去,直到接收到訊號量
            Thread thread2 = new Thread(AutoResetEventHandlerTwo);
            thread2.Start();
            Thread.Sleep(3000);//等待3秒
private static void AutoResetEventHandler()
        {
            Console.WriteLine("當前執行緒id" + Thread.CurrentThread.ManagedThreadId);
            auto.WaitOne();//阻塞執行緒
            Console.WriteLine("等待一秒後執行");
        }
private static void AutoResetEventHandlerTwo()
        {
            auto.WaitOne();//阻塞執行緒
            Console.WriteLine("我是第二個等待執行");
        }

ManualResetEvent

ManualResetEvent 與上面的AutoResetEvent 類似在建構函式中也會傳入一個Boolean型別引數,不同的是訊號量的釋放,AutoResetEvent在訊號量釋放後會自動設定為無訊號狀態(未終止狀態),ManualResetEvent 需要我們手動呼叫Reset()方法將其設定為無訊號量狀態(未終止狀態),否則其會一直保持有訊號量狀態(終止狀態)ManualResetEvent 如果不手動重置訊號量狀態,阻塞執行緒將不會起作用,會立即執行。

private static ManualResetEvent manualReset = new ManualResetEvent(false);
private static ManualResetEvent manualReset = new ManualResetEvent(true);
Thread thread1 = new Thread(() => {
                manualReset.WaitOne();
                Console.WriteLine("最開始的執行");
            });
            thread1.Start();
            Thread.Sleep(3000);//休眠--等待三秒
            manualReset.Set();//釋放訊號量
            Thread thread2 = new Thread(ManualResetEventHandler1);
            thread2.Start();
            manualReset.Reset();//充值訊號量
            Thread thread3=new Thread(ManualResetEventHandler2);
            manualReset.Set();//釋放訊號量
            thread3.Start();
            manualReset.Reset();
private static void ManualResetEventHandler1()
        {
            manualReset.WaitOne();
            Console.WriteLine("第一次等待執行");
        }
        private static void ManualResetEventHandler2()
        {
            manualReset.WaitOne();
            Console.WriteLine("第二次等待執行");
        }

上面說到過,ManualResetEvent 建構函式與AutoResetEvent建構函式是一樣,通過bool型別的引數判讀 類的例項是否預設釋放訊號量,不同的是ManualResetEvent 需要手動呼叫Reset()方法。上面程式碼中,我們傳遞了一個false引數,呼叫了Set()方法釋放訊號量,然後再呼叫Reset()方法重置訊號量,如此反覆一次,ManualResetEventHandler2 會一直阻塞 直到我們釋放訊號量,才會繼續執行。

CountdownEvent

CountdownEvent 例項化是需要傳入一個int 型別作為InitialCount初始值,CountdownEvent訊號量的釋放很特別,只有當Countdown類的例項的CurrentCount等於0時才會釋放我們的訊號量,Signal()方法每次呼叫都會使得CurrentCount進行-1操作。Reset()方法會重置為例項化物件時傳遞的引數值,也可以Reset(100)對我們的InitialCount重新賦值。

private static CountdownEvent countdownEvent = new CountdownEvent(1000);
 CountReduce();
           // countdownThread.Start();
            Thread thread = new Thread(() => {
                countdownEvent.Wait();
                Console.WriteLine("直到CountdownEvent總數="+countdownEvent.CurrentCount+"我才執行");
                //CountdownEvent.CurrentCount//當前總數
                //CountdownEvent.AddCount()//新增1
                //CountdownEvent.AddCount(10);//新增指定數量
                //CountdownEvent.InitialCount//總數
                //CountdownEvent.Reset()//設定為InitialCount初始值
                //CountdownEvent.Reset(100)//設定為指定初始值
            });
            thread.Start();
private static async Task CountReduce()
        {
           await Task.Run(async () => {
               for(var i = 0; i < 1000; i++)
                {
                    await Task.Delay(100);//休眠100毫秒--等到100毫秒
                   //if (countdownEvent.CurrentCount < 10)
                   //{
                   //    countdownEvent.Reset(100);
                   //    CountReduce();
                   //}
                   countdownEvent.Signal();
                    Console.WriteLine("當前總數"+countdownEvent.CurrentCount);
                }
            });
        }

上面程式碼中我們有用到非同步方法但沒有等待結果,但是但是執行緒的委託方法中呼叫了 countdownEvent.Wait()來阻塞執行緒;,只有當我們的CurrentCount等於0時才會釋放訊號量執行緒才不會阻塞得以繼續執行(有感興趣的可以試試這部分的程式碼)

EventWaitHandle

本地事件等待控制程式碼是指建立EventWaitHandle 物件時指定EventResetMode列舉,可分為自動重置的事件等待控制程式碼和手動重置的事件等待控制程式碼。
關於事件等待控制程式碼,不涉及到.NET 事件以及委託和事件處理程式,我們可以看一下官方的宣告。

EvenetResetMode.AutoReset

看到AutoReset是不是想起了,我們上面的AutoResetEvent,其用法是一樣的。在建立EventWaitHanlde物件時來指定是否自動重置訊號狀態。此同步事件表示一個等待執行緒(阻塞執行緒)在收到訊號時自動重置訊號狀態。此事件向等待執行緒傳送訊號時,需要呼叫Set()方法

EvenetResetMode.ManualReset

在建立EventWaitHandle物件時指定手動重置訊號狀態。事件收到訊號時手動重置訊號狀態,呼叫Set()方法釋放訊號。在呼叫ReSet()方法前,在此事件等待控制程式碼上的一個或多個等待執行緒(阻塞執行緒)收到訊號,立即繼續執行,並且此時的等待事件控制程式碼一直時保持訊號狀態(終止狀態)。這裡有個注意點,EventReseMode.ManualReset等待控制程式碼上有一個或多個等待執行緒,我們要注意Rese()的時機,等待執行緒恢復執行前是需要一定的執行時間的,我們無法判斷那個等待執行緒恢復到執行前,在呼叫Reset()方法可能會中斷等待執行緒的執行。如果我們希望在所有的等待執行緒都執行完後開啟新的執行緒,就必須將他組織到等待執行緒都完成後去傳送新的訊號量執行新的任務。
這裡我們可以看看官方的說法

private static EventWaitHandle EventWaitHandle=new EventWaitHandle(false,EventResetMode.ManualReset);
Thread thread1 = new Thread(EventWaitHandle1);
            Thread thread2 = new Thread(EventWaitHandle2);
            Thread thread3 = new Thread(EventWaitHandle3);
            thread1.Start();
            thread2.Start();
            thread3.Start();
            EventWaitHandle.Set();
            Thread thread4 = new Thread(EventWaitHandl4);
            Thread.Sleep(1000);
            thread4.Start();
            EventWaitHandle.Reset();
 private static void EventWaitHandle1()
        {
            EventWaitHandle.WaitOne();
            Thread.Sleep(1000);
            Console.WriteLine("我是第1個EventWaitHandle");
        }
        private static void EventWaitHandle2()
        {
            EventWaitHandle.WaitOne();
            Thread.Sleep(2000);
            Console.WriteLine("我是第2個EventWaitHandle");
        }
        private static void EventWaitHandle3()
        {
            EventWaitHandle.WaitOne();
            Thread.Sleep(3000);
            Console.WriteLine("我是第3個EventWaitHandle");
        }
        private static void EventWaitHandl4()
        {
            Thread.Sleep(3000);
            EventWaitHandle.WaitOne();
            Console.WriteLine("我是第4個EventWaitHandle");
        }

這裡著重說一下EventWaitHandle4,從上面可以看到,在thread4執行緒開始後就開始呼叫了Reset方法,並且在EventWaitHandle裡面休眠了三秒,這時候EventWaitHandle無法接收到訊號量會一直等待下去直到接收到新的訊號量。

Semaphore

Semaphore 可以限制同時進入的執行緒數量。Semaphore 的建構函式有兩個int 型別的引數,第一是指允許同時進入執行緒的個數,第二個是指最多與同時進入執行緒的個數,並且第二個引數時不能小於第一個引數(畢竟同時進入的不能大於最大能容納下的)。WaitOne()方法這裡的與上面幾個訊號量有點小小的不同,每呼叫一次Semaphore釋放的訊號燈數量減一,當訊號燈數量為0時會阻塞執行緒,Release()方法會對我們的訊號燈數量進行加一操作(釋放訊號燈),也可以呼叫Release(int i)來指定釋放的訊號燈數量。這裡有個注意點,我們可以在程式中多次呼叫Release方法(),但要保證在程式中釋放的訊號量不能大於最大訊號量。

private static Semaphore semaphore = new Semaphore(2, 5);//本地訊號燈
for (var i = 0; i < 12; i++)
            {
                Thread thread = new Thread(new ParameterizedThreadStart(Semaphorehandle));
                thread.Start(i);
            }
 private static void Semaphorehandle(Object i)
        {
            semaphore.WaitOne();
            Console.WriteLine((int)i + "進入了執行緒");
            Thread.Sleep(2000);
            Console.WriteLine((int)i + "準備離開執行緒");
            if ((int)i >1)
            {
                Console.WriteLine(semaphore.Release());
                return;
            }
            semaphore.Release(2);
        }

這裡插一句——多執行緒執行是沒有特定的順序的、是不可預測的。
Semaphore訊號燈有兩種:本地訊號燈和命名系統訊號燈。本地訊號燈僅存在於程式中(上面的例子中使用的是本地訊號燈)。命名系統訊號燈是存在與整個作業系統的,一般用於同步程式的活動。
c#多執行緒的訊號量就先到這了。
也可以看大佬的教程
本文涉及到的Demo程式碼

相關文章