C# 多線程學習系列四之ThreadPool取消、超時子線程操作以及ManualResetEvent和AutoResetEvent信號量的使用


1、簡介

雖然ThreadPool、Thread能開啟子線程將一些任務交給子線程去承擔,但是很多時候,因為某種原因,比如子線程發生異常、或者子線程的業務邏輯不符合我們的預期,那么這個時候我們必須關閉它,而不是讓它繼續執行,消耗資源.讓CPU不在把時間和資源花在沒有意義的代碼上.

 

2、主線程取消所有子線程執行的簡單代碼演示和原理分析

(1)、代碼演示

復制代碼
        static void Main(string[] args)
        {
            //顯示定義一個取消輔助線程的操作
            CancellationTokenSource ctsToken = new CancellationTokenSource();
            ThreadPool.QueueUserWorkItem(o => EoworkOne(ctsToken.Token));
            ThreadPool.QueueUserWorkItem(o => EoworkTwo(ctsToken.Token));
            ctsToken.Cancel();
            Console.Read();
        }

        /// <summary>
        /// 輔助線程一
        /// </summary>
        /// <param name="token"></param>
        static void EoworkOne(CancellationToken token)
        {
            //判斷主線程是否調用了CancellationTokenSource實例的Cancel方法
            //相當於判斷主線程是否傳遞給輔助線程一一個取消標記,如果你去看源碼,你會發現,里面有個有趣的類Timer,so,你懂的!結合之前的文檔,可以猜測這個時間很有可能是CPU切換上線文的時間
//每當過了這個時間,該子線程就去判斷主線程有沒有傳遞給它取消的信號.當然這只是我的猜測,哈哈
if (token.IsCancellationRequested) { //如果主線程傳遞給輔助線程一一個取消操作標記,執行下面的代碼 Console.WriteLine("主線程調用了Cancel方法,所以輔助線程一獲取了主線程取消輔助線程一的標記,但是並不會真正的關閉當前線程"); Console.WriteLine("輔助線程一執行return操作,自己顯示的退出,那么接下去的方法都不會被執行"); return; } } /// <summary> /// 輔助線程二 /// </summary> /// <param name="token"></param> static void EoworkTwo(CancellationToken token) { //判斷主線程是否調用了CancellationTokenSource實例的Cancel方法 //相當於判斷主線程是否傳遞給輔助線程一一個取消標記 if (token.IsCancellationRequested) { //如果主線程傳遞給輔助線程一一個取消操作標記,執行下面的代碼 Console.WriteLine("主線程調用了Cancel方法,所以輔助線程二獲取了主線程取消輔助線程二的標記,但是並不會真正的關閉當前線程"); } //因為當主線程傳遞給輔助線程二一個取消標記,但是上面的if語句塊,並沒有執行return操作,所以下面的語句還是會繼續執行 Console.WriteLine("輔助線程二獲得取消標記操作后,並沒有執行顯示的return操作,所以輔助線程二繼續執行"); }
復制代碼

 

(2)、原理分析

 第一步:創建一個CancellationTokenSource對象實例,該對象包含了所有關於取消子線程有關的所有狀態

CancellationTokenSource ctsToken = new CancellationTokenSource();

 第二步:將CancellationTokenSource對象實例的CancellationToken對象實例傳遞給需要進行取消操作的所有子線程.並且可以通過這個CancellationToken對象實例關聯到CancellationTokenSource對象實例.

ThreadPool.QueueUserWorkItem(o => EoworkOne(ctsToken.Token));
ThreadPool.QueueUserWorkItem(o => EoworkTwo(ctsToken.Token));

 第三步:當主線程調用CancellationTokenSource對象實例的Cancel方法,所有的子線程通過調用CancellationToken對象實例的IsCancellationRequested屬性,該屬性定時去獲取初始線程(主線程)是否執行了CancellationTokenSource對象實例的Cancel方法,如果調用了,該屬性為true。這時可以理解為子線程到主線程的取消信號,可以通過調用return方法來終止子線程的操作.

復制代碼
   //判斷主線程是否調用了CancellationTokenSource實例的Cancel方法
   //相當於判斷主線程是否傳遞給輔助線程一一個取消標記
   if (token.IsCancellationRequested)
   {
       //如果主線程傳遞給輔助線程一一個取消操作標記,執行下面的代碼
       Console.WriteLine("主線程調用了Cancel方法,所以輔助線程一獲取了主線程取消輔助線程一的標記,但是並不會真正的關閉當前線程");
       Console.WriteLine("輔助線程一執行return操作,自己顯示的退出,那么接下去的方法都不會被執行");
       return;
   }
復制代碼

 

3、如果創建一個不能被取消的子線程

通過給子線程傳遞一個CancellationToken.None實例,該子線程無法被取消,原因很簡單,CancellationToken.None實例沒有關聯的CancellationTokenSource對象實例,所以無法調用Cancel方法顯示取消.所以子線程調用token.IsCancellationRequested屬性,該屬性永遠為false.調用token.CanBeCanceled屬性也為false.

復制代碼
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(o => EoworkOne(CancellationToken.None));
            Console.Read();
        }

        /// <summary>
        /// 輔助線程一
        /// </summary>
        /// <param name="token"></param>
        static void EoworkOne(CancellationToken token)
        {
            if (token.IsCancellationRequested)
            {
                //永遠無法執行
            }
            Console.WriteLine("輔助線程一能被取消嗎?{0}",token.CanBeCanceled?"能":"不能");
            Console.WriteLine("通過CancellationToken.None實例創建的子線程無法被取消");
        }
復制代碼

 

4、初始線程(主線程)調用給CancellationTokenSource對象實例的Cancel方法添加回調函數

通過調用CancellationToken實例的Register方法來實現這個功能.

復制代碼
        static void Main(string[] args)
        {
            CancellationTokenSource ctsToken = new CancellationTokenSource();
            ThreadPool.QueueUserWorkItem((o => eowOne(ctsToken.Token)));
            ctsToken.Token.Register(() => { Console.WriteLine("ctsToken實例調用Cancel方法之后執行的回調函數一"); });
            ctsToken.Token.Register(() => { Console.WriteLine("ctsToken實例調用Cancel方法之后執行的回調函數二"); });
            ctsToken.Cancel();
            Console.Read();
        }

        /// <summary>
        /// 輔助線程一
        /// </summary>
        static void eowOne(CancellationToken token)
        {
            Thread.Sleep(2000);//模擬處理需要長時間做的任務
            Console.WriteLine("輔助線程一做完了它的事");
        }
復制代碼

通過輸出,可以發現,在給CancellationTokenSource實例的Token注冊完回調函數后,調用CancellationTokenSource實例的Cancel方法,立刻執行回調函數,但是,主線程並沒有等子線程執行完畢,在執行注冊的回調.而是直接執行回調。說明線程池線程在管理子線程何時執行完畢是非常無力的.

 

5、關於處理CancellationTokenSource實例調用Cancel方法后,獲取所有回調函數的未處理的異常

(1)、給CancellationTokenSource的Cancel方法傳遞true

復制代碼
        static void Main(string[] args)
        {
            CancellationTokenSource ctsToken = new CancellationTokenSource();
            ThreadPool.QueueUserWorkItem((o => eowOne(ctsToken.Token)));
            ctsToken.Token.Register(() => { throw new Exception("回調函數一拋出的異常"); });
            ctsToken.Token.Register(() => { throw new Exception("回調函數二拋出的異常"); });
            ctsToken.Cancel(true);
            Console.Read();
        }

        /// <summary>
        /// 輔助線程一
        /// </summary>
        static void eowOne(CancellationToken token)
        {
            Thread.Sleep(2000);//模擬處理需要長時間做的任務
            Console.WriteLine("輔助線程一做完了它的事");
        }
復制代碼

調試代碼發現,執行到第一個回調函數,拋出異常,程序直接跳出,不再執行第二個函數.所以可以得出結論,為Cancel方法傳遞true,它只會捕獲第一個異常,不再執行第二個異常.

 

(2)、給CancellationTokenSource的Cancel方法傳遞false

傳遞false后,程序會分別執行所有的回調,並拋出一個System.AggregateException異常,回調函數的異常會被追加到到其InnerExceptions屬性中.

 

6、ManualResetEvent、AutoResetEvent阻塞線程信號量使用

關於強制主線程等待子線程完成任務之后執行的方法主要用這兩個信號量來實現,注意主線程只能等待一個子線程的完成,不能等待兩個子線程完成,這里我試了很多種辦法,都不行,可能對它的Api還不夠了解.所以用的時候需要考慮這點.使用ManualResetEvent信號量,主線程只能等待一個子線程的完成.

用法如下:

(1)、ManualResetEvent

復制代碼
        static  void Main(string[] args)
        {
            ManualResetEvent mre = new ManualResetEvent(false);//創建ManualResetEvent信號量,主線這里構造函數必須傳遞false
            ThreadPool.QueueUserWorkItem((o => eowOne(mre)));//開啟輔助線程
            mre.WaitOne();//讓主線程等待子線程的完成
            Console.WriteLine("主線程繼續做它的事情!");
            Console.Read();
        }

        /// <summary>
        /// 輔助線程一
        /// </summary>
        static void eowOne(ManualResetEvent mre)
        {
            var watch = Stopwatch.StartNew();
            Thread.Sleep(2000);
            watch.Stop();
            Console.WriteLine("輔助線程一做完了它的事,耗時:{0}", watch.ElapsedMilliseconds/1000);
            mre.Set();//告訴主線程子線程執行完了,如果不給ManualResetEvent實例調用這個方法,主線程會一直等待子線程調用ManualResetEvent實例的Set方法
        }
復制代碼

如果子線程不調用Set方法,子線程代碼如下:

復制代碼
        /// <summary>
        /// 輔助線程一
        /// </summary>
        static void eowOne(ManualResetEvent mre)
        {
            var watch = Stopwatch.StartNew();
            Thread.Sleep(2000);
            watch.Stop();
            Console.WriteLine("輔助線程一做完了它的事,耗時:{0}", watch.ElapsedMilliseconds/1000);
        }
復制代碼

子線程做完了它的事情,但是沒有調用ManualResetEvent實例的Set方法,所以,主線程會一直等待.這里主線程就被阻塞了.

結論:

(1)、當給ManualResetEvent實例的構造函數傳false的時候,主線程調用ManualResetEvent實例的WaitOne方法時,如果子線程沒有調用ManualResetEvent實例的Set方法,那么主線程會阻塞.

(2)、如果子線程調用了ManualResetEvent實例的Set方法,那么主線程調用ManualResetEvent實例的WaitOne方法,那么主線程會接收到一個子線程已經完成的信號,並且繼續執行.不會阻塞.

(3)、無論怎么樣主線程都會阻塞,只是不調用Set,主線程永遠阻塞了,執行不下去了,調用Set,主線程還是會阻塞,但是當子線程完成工作之后,它會繼續執行.

 

(2)、ManualResetEvent的ReSet方法

讓ManualResetEvent實例回歸初始狀態

復制代碼
        static  void Main(string[] args)
        {
            ManualResetEvent mre = new ManualResetEvent(false);//創建ManualResetEvent信號量,主線這里構造函數必須傳遞false
            ThreadPool.QueueUserWorkItem((o => eowOne(mre)));//開啟輔助線程一
            mre.WaitOne();//讓主線程等待輔助線程一的完成
            mre.Reset();//調用ReSet方法,讓ManualResetEvent回到初始狀態,如果不使用這個方法,主線程不會等待輔助線程二,直接執行,因為輔助線程一已經調用了mre.Set方法
            ThreadPool.QueueUserWorkItem((o => eowTwo(mre)));//開啟輔助線程二
            mre.WaitOne();//讓主線程等待子線程輔助線程二的完成
            Console.WriteLine("主線程繼續做它的事情!");
            Console.Read();
        }

        /// <summary>
        /// 輔助線程一
        /// </summary>
        static void eowOne(ManualResetEvent mre)
        {
            var watch = Stopwatch.StartNew();
            Thread.Sleep(2000);
            watch.Stop();
            Console.WriteLine("輔助線程一做完了它的事,耗時:{0}", watch.ElapsedMilliseconds/1000);
            mre.Set();
        }

        /// <summary>
        /// 輔助線程二
        /// </summary>
        static void eowTwo(ManualResetEvent mre)
        {
            var watch = Stopwatch.StartNew();
            Thread.Sleep(2000);
            watch.Stop();
            Console.WriteLine("輔助線程二做完了它的事,耗時:{0}", watch.ElapsedMilliseconds / 1000);
            var status = mre.Set();
            if (status)
            {
                mre.Reset();
            }
        }
復制代碼

ok,主線程會依次等待兩個線程順序執行完它們的事情,你可能發現一個問題.這和同步有什么區別!哈哈,有區別,如果主線程執行的任務足夠耗時,而且執行到某一個時段,需要判斷子線程是否完成,獲取需要子線程的返回值(當然TreadPool不能很友好的拿到返回值),這個時候這種做法就有優勢了兩個線程各自承擔自己的事情,互不干擾,需要協同操作了,主線程調用下Wait方法,確認子線程正確的完成了它的操作之后,繼續執行主線程的任務..所以需謹慎使用.主線程如果啥都不干,光光去等待子線程完成,這種情況和同步就沒有刪么區別了.所以這個過程可能會卡界面.也有可能不卡.

 

(3)、AutoResetEvent信號量

AutoResetEvent和ManualResetEvent大體上沒什么區別,都是阻塞主線程,但是ManualResetEvent需要每次調用ReSet方法而AutoResetEvent不用.

復制代碼
        static  void Main(string[] args)
        {
            AutoResetEvent mre = new AutoResetEvent(false);//創建ManualResetEvent信號量,主線這里構造函數必須傳遞false
            ThreadPool.QueueUserWorkItem((o => eowOne(mre)));//開啟輔助線程一
            mre.WaitOne();//讓主線程等待輔助線程一的完成
            ThreadPool.QueueUserWorkItem((o => eowTwo(mre)));//開啟輔助線程二
            mre.WaitOne();//讓主線程等待子線程輔助線程二的完成
            Console.WriteLine("主線程繼續做它的事情!");
            Console.Read();
        }

        /// <summary>
        /// 輔助線程一
        /// </summary>
        static void eowOne(AutoResetEvent mre)
        {
            var watch = Stopwatch.StartNew();
            Thread.Sleep(2000);
            watch.Stop();
            Console.WriteLine("輔助線程一做完了它的事,耗時:{0}", watch.ElapsedMilliseconds/1000);
            mre.Set();
        }

        /// <summary>
        /// 輔助線程二
        /// </summary>
        static void eowTwo(AutoResetEvent mre)
        {
            var watch = Stopwatch.StartNew();
            Thread.Sleep(2000);
            watch.Stop();
            Console.WriteLine("輔助線程二做完了它的事,耗時:{0}", watch.ElapsedMilliseconds / 1000);
            var status = mre.Set();
            if (status)
            {
                mre.Reset();
            }
        }
復制代碼

 

 


免責聲明!

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



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