並行程式設計 --- 號誌執行緒同步

2023-07-18 09:00:40

引言

上文編碼技巧 --- 同步鎖物件的選定中,提到了在C#中,讓執行緒同步有兩種方式:

  • 鎖(lock、Monitor等)
  • 號誌(EventWaitHandle、Semaphore、Mutex)

加鎖是最常用的執行緒同步的方法,就不再討論,本篇主要討論使用號誌同步執行緒。

WaitHandle介紹

實際上,再C#中 EventWaitHandleSemaphoreMutex 都是抽象類 WaitHandle 的派生類,它提供了一組等待訊號的方法和屬性。如下圖:

主要包含靜態方法 SignalAndWait()WaitAll()WaitAny()及一個虛方法WaitOne()。下面介紹一個這幾個方法。

介紹這些方法之前,先簡單介紹一下 WaitHandle 的派生類 EventWaitHandle,該派生類有兩個實現類 AutoResetEventManualResetEvent,其方法列表如下:

重點說一下,Set()Reset():

  • Set()方法設定事件為有訊號狀態:當呼叫 Set() 時,它將被設定為終止狀態,並允許一個或多個等待該事件的執行緒繼續執行。
  • Reset()方法設定事件為無訊號狀態:當呼叫 Reset() 時,它將被設定為非終止狀態,所有想要等待該事件的執行緒都將被阻塞,直到呼叫 Set() 方法使其變為終止狀態。

注意:這裡的有訊號,無訊號的意思類似於紅綠燈,有訊號你才能夠通行,對於執行緒來說,有訊號意味著可以接著往下執行,無訊號則阻塞等待訊號。

接下來的程式碼段演示皆使用 AutoResetEvent 進行演示。

SignalAndWait()

當呼叫 WaitHandle 的靜態方法 SignalAndWait() 時,會使得當前執行緒等待一個 WaitHandle 物件的訊號,同時設定另一個 WaitHandle 物件為有訊號狀態。當第一個 WaitHandle 物件收到訊號時,當前執行緒繼續執行,同時第二個 WaitHandle 物件變為無訊號狀態。

static AutoResetEvent event1 = new AutoResetEvent(false);
static AutoResetEvent event2 = new AutoResetEvent(false);

static void Main(string[] args)
{
    Thread t1 = new Thread(new ThreadStart(Worker1));
    Thread t2 = new Thread(new ThreadStart(Worker2));

    t1.Start();
    t2.Start();

    Console.ReadLine();
}

static void Worker1()
{
    Console.WriteLine("執行緒1開始執行……");

    event1.WaitOne(); // 等待事件1的發生

    Console.WriteLine("執行緒1收到事件1的訊號,繼續執行……");

    WaitHandle.SignalAndWait(event1, event2); // 傳送事件2的訊號並等待事件2的發生

    Console.WriteLine("執行緒1收到事件2的訊號,繼續執行……");
}

static void Worker2()
{
    Console.WriteLine("執行緒2開始執行……");

    Thread.Sleep(2000); // 模擬執行緒2的執行時間

    Console.WriteLine("執行緒2發出事件1的訊號……");

    event1.Set(); // 傳送事件1的訊號

    Thread.Sleep(2000); // 模擬執行緒2的執行時間

    Console.WriteLine("執行緒2發出事件2的訊號……");

    WaitHandle.SignalAndWait(event2, event1); // 傳送事件1的訊號並等待事件1的發生

    Console.WriteLine("執行緒2收到事件1的訊號,繼續執行……");
}

輸出:

執行緒1開始執行……
執行緒2開始執行……
執行緒2發出事件1的訊號……
執行緒1收到事件1的訊號,繼續執行……
執行緒2發出事件2的訊號……
執行緒2收到事件1的訊號,繼續執行……
執行緒1收到事件2的訊號,繼續執行……

WaitAll()

當呼叫 WaitHandle 的靜態方法 WaitAll() 時,它可以等待多個WaitHandle物件的訊號,直到所有物件都收到訊號或等待超時。

static AutoResetEvent[] events = new AutoResetEvent[3]
{
    new AutoResetEvent(false),
    new AutoResetEvent(false),
    new AutoResetEvent(false)
};

static void Main(string[] args)
{
    Thread t1 = new Thread(new ThreadStart(Worker1));
    Thread t2 = new Thread(new ThreadStart(Worker2));

    t1.Start();
    t2.Start();

    Console.ReadLine();
}

static void Worker1()
{
    Console.WriteLine("執行緒1開始執行……");

    WaitHandle.WaitAll(events); // 等待所有事件的發生

    Console.WriteLine("執行緒1收到所有事件的訊號,繼續執行……");
}

static void Worker2()
{
    Console.WriteLine("執行緒2開始執行……");

    Thread.Sleep(2000); // 模擬執行緒2的執行時間

    Console.WriteLine("執行緒2發出事件1的訊號……");

    events[0].Set(); // 傳送事件1的訊號

    Thread.Sleep(2000); // 模擬執行緒2的執行時間

    Console.WriteLine("執行緒2發出事件2的訊號……");

    events[1].Set(); // 傳送事件2的訊號

    Thread.Sleep(2000); // 模擬執行緒2的執行時間

    Console.WriteLine("執行緒2發出事件3的訊號……");

    events[2].Set(); // 傳送事件3的訊號
}

輸出:

執行緒1開始執行……
執行緒2開始執行……
執行緒2發出事件1的訊號……
執行緒2發出事件2的訊號……
執行緒2發出事件3的訊號……
執行緒1收到所有事件的訊號,繼續執行……

WaitAny()

當呼叫 WaitHandle 的靜態方法 WaitAny() 時,它可以等待多個WaitHandle物件中的任意一個物件收到訊號,直到有一個物件收到訊號或等待超時。

static AutoResetEvent[] events = new AutoResetEvent[3]
{
    new AutoResetEvent(false),
    new AutoResetEvent(false),
    new AutoResetEvent(false)
};

static void Main(string[] args)
{
    Thread t1 = new Thread(new ThreadStart(Worker1));
    Thread t2 = new Thread(new ThreadStart(Worker2));

    t1.Start();
    t2.Start();

    Console.ReadLine();
}

static void Worker1()
{
    Console.WriteLine("執行緒1開始執行……");

    WaitHandle.WaitAny(events); // 等待任意事件的發生

    Console.WriteLine("執行緒1收到任意事件的訊號,繼續執行……");
}

static void Worker2()
{
    Console.WriteLine("執行緒2開始執行……");

    var randomIndex = new Random().Next(0, 2);

    Console.WriteLine("執行緒2發出任意一個事件的訊號……");

    events[randomIndex].Set(); //傳送任意一個事件的訊號
}

輸出:

執行緒1開始執行……
執行緒2開始執行……
執行緒2發出任意一個事件的訊號……
執行緒1收到任意事件的訊號,繼續執行……

WaitOne()

WaitOne()方法上文中其實已經用到了,它就表示阻塞當前執行緒,等待當前 WaitHandle 物件收到訊號,直到物件收到訊號或等待超時。如果WaitHandle物件收到訊號,WaitOne()方法返回true,否則返回false。使用簡單就不在貼程式碼段。

派生類的異同

上面已經提到了EventWaitHandleSemaphoreMutex 都是抽象類 WaitHandle 的派生類,它們的作用類似,但在使用和實現上有一些不同。下面我們來簡單介紹下它們的異同點。

  1. EventWaitHandle:

    EventWaitHandle 有兩種型別:AutoResetEventManualResetEvent。它們的區別在於AutoResetEvent 在有訊號時只通知一個等待執行緒,而 ManualResetEvent 在有訊號時通知所有等待執行緒。
    兩者設定為終止狀態的方式都是呼叫 Set() 方法。

  2. Semaphore

    Semaphore 可以用於多個執行緒之間的資源控制。Semaphore 可以控制同時存取共用資源的執行緒數量。設定為終止狀態的方式是呼叫 Release() 方法。

  3. Mutex

    Mutex 可以用於多個執行緒之間的互斥存取共用資源。Mutex 可以保證同一時間只有一個執行緒可以存取共用資源。設定為終止狀態的方式是呼叫 ReleaseMutex() 方法。