線程系列09,線程的等待、通知,以及手動控制線程數量



當一個線程直到收到另一個線程的通知才執行相關的動作,這時候,就可以考慮使用"事件等待句柄(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("收到指示~~");
        }
    }

42

○ 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);
                }
            }
        }
    }

43

 

把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)
            {
                //從隊列中獲取task
                string task = null;
                lock (_locker)
                {
                    if (_tasks.Count > 0)
                    {
                        task = _tasks.Dequeue();
                        if(task == null) return;
                    }
                }
                //如果task不為null,模擬執行task
                if (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");
            }
        }
    }

44

 

□ 手動控制線程的數量

 

■ 使用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);
        }
    }

45

○ 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();
        }
    }

46

○ 在CountdownEvent的構造函數中設置允許的最大線程數
○ Signal方法表示計數一次


總結:

○ 使用AutoResetEvent類,可以讓一個線程等待另一個線程的通知,2個線程互相通知等待,一個線程等待隊列中的多個任務通知
○ 使用ManualResetEvent類,手動控制任意多的線程數量
○ CountdownEvent類,手動控制固定數量的線程數量

 

 

線程系列包括:

線程系列01,前台線程,后台線程,線程同步

線程系列02,多個線程同時處理一個耗時較長的任務以節省時間

線程系列03,多線程共享數據,多線程不共享數據

線程系列04,傳遞數據給線程,線程命名,線程異常處理,線程池

線程系列05,手動結束線程

線程系列06,通過CLR代碼查看線程池及其線程

線程系列07,使用lock語句塊或Interlocked類型方法保證自增變量的數據同步

線程系列08,實現線程鎖的各種方式,使用lock,Montor,Mutex,Semaphore以及線程死鎖

線程系列09,線程的等待、通知,以及手動控制線程數量

線程系列10,無需顯式調用線程的情形


免責聲明!

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



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