🚫 Ad Blocker Detected

Please disable your AD blocker to continue using this site. Ads help us keep the content free! please press keyboard F5 to refresh page after disabled AD blocker

請關閉廣告攔截器以繼續使用本網站。廣告有助於我們保證內容免費。謝謝! 關閉後請按 F5 刷新頁面

0%

高併發系統系列-多執行緒中順序 by EventWaitHandle

前言

在多執行緒系統中,Thread的執行和長短是交由OS來幫我處理了

假如有一個面試題目是

  • 目前有三個Thread 每個Thread個別負責Print “A”,”B”,”C”

  • 要求:利用三個Thread並按照A,B,C順序打印出20次資訊,中間不能錯號

    ex:
    A
    B
    C
    A
    B
    C

程式碼框架如下,在Main函式建立3個Threads分別負責A,B,C字母打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Program
{
static void Main(string[] args)
{
Alternate c = new Alternate();
var t1 = new Thread(c.CallA);
var t2 = new Thread(c.CallB);
var t3 = new Thread(c.CallC);

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

t1.Join();
t2.Join();
t3.Join();

Console.ReadKey();
}
}
public class Alternate
{
public void CallA()
{
for (int i = 0; i < 20; i++)
{
Console.WriteLine("A");
}

}

public void CallB()
{
for (int i = 0; i < 20; i++)
{
Console.WriteLine("B");
}

}
public void CallC()
{
for (int i = 0; i < 20; i++)
{
Console.WriteLine("C");
}
}
}

因為Thread被呼叫順序是由OS跟CPU來決定,目前執行如下圖所以目前打印出來的順序是無序的

期望可以打印出如下圖

讓Thread可以有順的執行打印.

假如是你會怎麼做?

問題分析

如果要完成上面需求,我們會希望程式如下執行

  • 在執行ThreadA時,ThreadB,ThreadC在等待,執行完ThreadA時呼叫ThreadB起來動作
  • 在執行ThreadB時,ThreadC,ThreadA在等待,執行完ThreadB時呼叫ThreadC起來動作
  • 在執行ThreadC時,ThreadB,ThreadA在等待,執行完ThreadC時呼叫ThreadA起來動作

核心動作:每個瞬間只有一個Thread可以動作,執行完呼叫下一個Thread做事情,自己在進行等待喚醒

如果你有建構多執行緒系統的話,看到上面動作應該就知道本題的核心類別是哪個了吧?

沒錯就是EventWaitHandle

EventWaitHandle

如果要談起AutoResetEvent就不得不提下面兩個類別

  • AutoResetEvent:執行緒同步處理事件會在發出訊號時,釋出一個等候執行緒之後就自動重設
  • ManualResetEvent:執行緒同步處理事件可以釋出多個等候執行緒,收到訊號時必須手動重設 ,不然其他執行緒會直接放行

我用現實生活中例子來解釋AutoResetEventManualResetEvent

  • AutoResetEvent:捷運或火車的閘門一次只能放行一個並在放行完後會馬上關門
  • ManualResetEvent:大門必須要是手動關閉房門,不然就會一直放行別人通過

我先來說說為什麼可以透過EventWaitHandle來解決問題.

一樣我們把要完成的思路先畫出來,假如我們可以在每一個Thread跟Thread之間設定一個閘門(圖中的小框框),每次呼叫完就把下一關閘門打開叫醒另一個Thread去做事情(在把之前的閘門關起來等待
上一位呼叫)

例如:

現在ThreadA剛剛執行完(打印出A)接著會去叫醒ThreadB做事情後,ThreadA繼續等待ThreadC做完事情叫他.

上面情境就需要使用AutoResetEvent來當作我們閘門跟叫醒Thread動作.

問題解答

本次我使用到AutoResetEvent兩個常用的method.

  • WaitOne:Blocking目前Thread,直到目前WaitHandle收到訊號為止。
  • Set:將事件的狀態設定為未收到信號,讓一個或多個等候執行緒繼續執行。

簡單來說AutoResetEvent就是閘門

WaitOne幫助我們達成Thread Blocking動作(等待被喚醒),而喚醒交由Set別人呼叫你WaitOne的AutoResetEvent.

按照上面的圖我們會需要使用到三個AutoResetEvent(三個閘門)

另外我們需要知道目前在哪一個閘門,所以有一個Index變數(我們有看到他使用volatile關鍵字)有興趣在自行尋找用處因為不是本次重點我就不說明了.

我們把

  • ThreadA執行當作Index = 1
  • ThreadB執行當作Index = 2
  • ThreadC執行當作Index = 3

所以在一開始判斷是否是自己要執行Index,如果不是就等待被呼叫.

按照一開始例子我們設定index = 1,所以CallA動作不會被Blocking,就會接續打印出A並看到notifyB.Set();會叫醒ThreadB並且把Index設定成2

因為Index = 2,所以再一次跑迴圈ThreadA就會進行blocking,後面動作以此類推就會出現我們要的答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Alternate {

AutoResetEvent notifyA = new AutoResetEvent(false);
AutoResetEvent notifyB = new AutoResetEvent(false);
AutoResetEvent notifyC = new AutoResetEvent(false);

private volatile int index = 1;
public void CallA()
{
for (int i = 0; i < 20; i++)
{
if (index != 1)
notifyA.WaitOne();

Console.WriteLine("A");
index = 2;
notifyB.Set();
}

}

public void CallB()
{
for (int i = 0; i < 20; i++)
{
if (index != 2)
notifyB.WaitOne();

Console.WriteLine("B");
index = 3;
notifyC.Set();
}

}
public void CallC()
{
for (int i = 0; i < 20; i++)
{
if (index != 3)
notifyC.WaitOne();

Console.WriteLine("C");
Console.WriteLine("------------------------------");
index = 1;
notifyA.Set();
}
}
}

重購版本

上面那個本版雖然可以很好的完成需求,程式碼有很多重複的地方且可擴展性不佳.

下面版本是重購後的我們可以在Alternate建構子中撰寫對應列表(每個Thread要被誰喚醒跟做完事情要叫誰),並給上相對應編號來查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
 public class Alternate {
private class NotifyMap {
public AutoResetEvent Wait { get; set; }
public AutoResetEvent Notify { get; set; }
}
private Dictionary<int, NotifyMap> _notifyMapping;
private volatile int index = 0;

AutoResetEvent notifyA = new AutoResetEvent(false);
AutoResetEvent notifyB = new AutoResetEvent(false);
AutoResetEvent notifyC = new AutoResetEvent(false);

public Alternate()
{
_notifyMapping = new Dictionary<int, NotifyMap>
{
{ 1, new NotifyMap{ Wait = notifyA, Notify = notifyB} },
{ 2, new NotifyMap{ Wait = notifyB, Notify = notifyC} },
{ 3, new NotifyMap{ Wait = notifyC, Notify = notifyA} }
};

}


public void Call(CallerInfo caller)
{
for (int i = 0; i < 20; i++)
{
var key = index % _notifyMapping.Count + 1;
var notifyMap = _notifyMapping[caller.Index];
if (key != caller.Index)
notifyMap.Wait.WaitOne();
Console.WriteLine($"{caller.Name}");
index++;
notifyMap.Notify.Set();
}
}
}

經過重購後程式碼就變得簡單許多了.

小結

在multithread世界中我們要注意細節越來越多,本次介紹的AutoResetEvent可以用運的地方非常多(假如你有多個Thread需要順序執行)

因為Thread的呼叫順序我們不能掌控就必須考這個機制來幫我們完成.

程式碼我放在有需要可以拿去使用看看

__此文作者__:Daniel Shih(石頭)
__此文地址__: https://isdaniel.github.io/multithread-eventwaithandle/
__版權聲明__:本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 3.0 TW 許可協議。轉載請註明出處!

如果本文對您幫助很大,可街口支付斗內鼓勵石頭^^