當一個線程直到收到另一個線程的通知才執行相關的動作,這時候,就可以考慮使用"事件等待句柄(Event Wait Handles)"。使用"事件等待句柄"主要用到3個類: AutoResetEvent, ManualResetEvent以及CountdownEvent(.NET 4.0以后才有)。本篇包括:
※ 一個線程等待另一個線程的通知
※ 2個線程互相通知等待
※ 一個線程等待隊列中的多個任務通知
※ 手動控制線程的數量
□ 一個線程等待另一個線程的通知
最簡單的情景是:發出信號的線程只發出一次通知,等待的線程收到通知也只做一次事情。等待的線程肯定有一個等待方法,發出信號的線程必須有一個發出信號的方法,AutoResetEvent類提供了相關方法。
class Program{//true表示將初始狀態設置為終止狀態static EventWaitHandle _wait = new AutoResetEvent(false);static void Main(string[] args){new Thread(Waiter).Start();Thread.Sleep(1000);_wait.Set();//發出指示}static void Waiter(){Console.WriteLine("一切准備就緒,等待指示!");_wait.WaitOne();Console.WriteLine("收到指示~~");}}
○ AutoResetEvent就像地鐵入口的十字轉門,有票插入,就讓進,而且每次只讓一個人進。
○ 當調用WaitOne方法,表示該線程已被阻塞,正在等待信號,就像十字轉門旁等待進入的乘客。
○ 當調用Set方法,表示發出信號給等待線程,就像十字轉門收到車票,乘客可以通過。
關於AutoResetEvent:
○ 還可通過這種方式創建AutoResetEvent實例:var auto = new EventWaitHandle(false, EventResetMode.AutoReset);
○ 如果調用了Set方法,卻沒有其它線程調用WaitOne方法,這個handle會一直存在
○ 如果調用Set方法多次,卻有多個線程調用WaitOne方法,也只能讓這些線程挨個接收信號,即每次只有一個線程接收信號
○ WaitOne還有幾個接收時間間隔參數的重載方法,使用WaitOne(0)可以測試一個wait handle是否已經打開
○ GC自動回收wait handles
□ 2個線程互相通知等待
還有一種情形:發出信號的線程要發出多次通知,每一次需要確認等待線程收到后再發下一個通知。大概的過程就是:線程A第一次做事並發出通知,進入等待狀態;線程B收到通知,發出通知,通知線程A,線程B進入等待狀態;線程A收到線程B的通知,第二次做事並發出通知,進入等待狀態......2個線程互相通知,每個線程既是發出信號者,也是等待者。借助AutoResetEvent類可以解決此需求。
class Program{static EventWaitHandle _ready = new AutoResetEvent(false);static EventWaitHandle _go = new AutoResetEvent(false);static readonly object o = new object();private static string _msg;static void Main(string[] args){new Thread(DoSth).Start();//第一次等待直到另外一個線程准備好_ready.WaitOne();lock (o){_msg = "你好";}_go.Set();//第二次等待_ready.WaitOne();lock (o){_msg = "嗎";}_go.Set();//第三次_ready.WaitOne();lock (o){_msg = null;}_go.Set();}static void DoSth(){while (true){_ready.Set();_go.WaitOne();lock (o){if(_msg == null) return;Console.WriteLine(_msg);}}}}
把Main方法中的線程稱為主線程,把另一個線程稱為工作線程,2個線程是這樣工作的:
→主線程使用WaitOne方法第一次等待,說:“工作線程,我等在這里”
→工作線程使用Set方法,說:“主線程,我給你信號,你准備第一條信息吧”,並且又使用WaitOne方法讓自己等待,就說:“主線程,我給你信號了,我等在這里,准備接收你的第一條信息”,再看看暫時還沒有需要顯示的信息,於是作罷
→主線程收到工作線程的信號,設置第一條信息,然后使用Set方法,說"工作線程,我的第一條信息給你,給你信號",並且又使用WaitOne方法讓自己第二次等待,說:"工作線程,我給你信號了,我等在這里"
→工作線程又使用Set方法,說:“主線程,我給你信號,你去准備第二條信息吧”,並且又使用WaitOne方法讓自己等待,就說:“主線程,我已經給你信號了,我等在這里,准備接收你的第二條信息”,再看看這時有需要顯示的信息,就把信息打印了出來
→依次類推
□ 一個線程等待隊列中的多個任務通知
當一個等待的線程,需要逐個執行多個任務,就可以把任務放在隊列中。
通常把能實現實現上述需求的叫做"生產/消費隊列"。所謂的"生產"是指能把多個任務放到隊列中,所謂"消費"是指當任務逐一出列,再執行該任務。
class ProducerConsumerQueue : IDisposable{EventWaitHandle _ewh = new AutoResetEvent(false);private Thread _worker; //等待線程private readonly object _locker = new object();Queue<string> _tasks = new Queue<string>();//任務隊列public ProducerConsumerQueue(){_worker = new Thread(Work);_worker.Start();}//任務進入隊列public void EnqueueTask(string task){lock (_locker){_tasks.Enqueue(task);}//任務一旦進入隊列就發出信號_ewh.Set();}void Work(){while (true){//從隊列中獲取taskstring task = null;lock (_locker){if (_tasks.Count > 0){task = _tasks.Dequeue();if(task == null) return;}}//如果task不為null,模擬執行taskif (task != null){Console.WriteLine("正在執行線程任務 " + task);Thread.Sleep(1000); //模擬線程執行的過程}else//如果taks為null{_ewh.WaitOne();//等待信號}}}public void Dispose(){EnqueueTask(null); //發出信號讓消費線程退出_worker.Join();//讓消費線程借宿_ewh.Close();//釋放event wait handle}}
○ EnqueueTask方法,讓任務進入隊列,每個進入隊列的任務使用Set方法發出通知,產生任務的過程就是所謂的"生產"
○ Wokr方法,在沒有task的時候,使用WaitOne方法一直等待;當任務出列,就執行任務,執行任務的過程就是所謂的"消費"
○ 構造函數創建、啟動等待線程,讓等待線程一直工作者(通過無限循環)
客戶端調用。
class Program{static void Main(string[] args){using (ProducerConsumerQueue q = new ProducerConsumerQueue()){q.EnqueueTask("hello");for (int i = 0; i < 3; i++){q.EnqueueTask("報數" + i);}q.EnqueueTask("world");}}}
□ 手動控制線程的數量
■ 使用ManualResetEvent
如果把AutoResetEvent比作地鐵入口的十字轉門,一次只能允許一個人進入;ManualResetEvent可看作公司門衛,上班時間到,打開門可以讓多人進入。ManualResetEvent的Set方法就如同開門,任意多個線程可以進入,Reset方法如同關門,線程從此不能再進入。
創建ManualResetEvent實例有2種方式:
var manual1 = new ManualResetEvent (false);var manual2 = new EventWaitHandle (false, EventResetMode.ManualReset);
以下是EventWaitHandle的一個簡單應用:
class Program{static EventWaitHandle handle = new ManualResetEvent(false);static void Main(string[] args){handle.Set();new Thread(SaySth).Start("Hello");new Thread(SaySth).Start("World");Thread.Sleep(2000);handle.Reset();new Thread(SaySth).Start("Again");}static void SaySth(object data){handle.WaitOne();Console.WriteLine("我想說的是:" + data);}}
○ Set方法,相當於開門,其后面的2個線程有效
○ Reset方法,相當於關門,其后面的1個線程無效
■ 使用CountdownEvent
CountdownEvent也可以看作公司門衛,只不過,上班時間到,規定只允許若干個人進去。
class Program{static CountdownEvent _countdown = new CountdownEvent(2);static void Main(string[] args){new Thread(SaySth).Start("1");new Thread(SaySth).Start("2");}static void SaySth(object o){Thread.Sleep(1000);Console.WriteLine(o);_countdown.Signal();}}
○ 在CountdownEvent的構造函數中設置允許的最大線程數
○ Signal方法表示計數一次
總結:○ 使用AutoResetEvent類,可以讓一個線程等待另一個線程的通知,2個線程互相通知等待,一個線程等待隊列中的多個任務通知
○ 使用ManualResetEvent類,手動控制任意多的線程數量
○ CountdownEvent類,手動控制固定數量的線程數量
線程系列包括:





