天天看點

[.Net線程處理系列]專題二:線程池中的工作者線程

目錄:

一、上節補充

二、CLR線程池基礎

三、通過線程池的工作者線程實作異步

四、使用委托實作異步

五、任務

六、小結

對于Thread類還有幾個常用方法需要說明的。

1.1 Suspend和Resume方法

    這兩個方法在.net Framework 1.0 的時候就支援的方法,他們分别可以挂起線程和恢複挂起的線程。但在.net Framework 2.0以後的版本中這兩個方法都過時了,MSDN的解釋是這樣:

警告:

不要使用 Suspend 和 Resume 方法來同步線程的活動。您無法知道挂起線程時它正在執行什麼代碼。如果您在安全權限評估期間挂起持有鎖的線程,則 AppDomain中的其他線程可能被阻止。如果您線上程正在執行類構造函數時挂起它,則 AppDomain中嘗試使用該類的其他線程将被阻止。這樣很容易發生死鎖。

對于這個解釋可能有點抽象吧,讓我們來看看一段代碼可能會清晰點:

class Program  

    {  

        static void Main(string[] args)  

        {  

            // 建立一個線程來測試  

            Thread thread1 = new Thread(TestMethod);        

            thread1.Name = "Thread1";     

            thread1.Start();      

            Thread.Sleep(2000);  

            Console.WriteLine("Main Thread is running");  

            ////int b = 0;  

            ////int a = 3 / b;  

            ////Console.WriteLine(a);  

            thread1.Resume();       

            Console.Read();  

        }  

        private static void TestMethod()  

        {       

            Console.WriteLine("Thread: {0} has been suspended!", Thread.CurrentThread.Name);  

            //将目前線程挂起  

            Thread.CurrentThread.Suspend();            

            Console.WriteLine("Thread: {0} has been resumed!", Thread.CurrentThread.Name);  

    } 

     在上面這段代碼中thread1線程是在主線程中恢複的,但當主線程發生異常時,這時候就thread1一直處于挂起狀态,此時thread1所使用的資源就不能釋放(除非強制終止程序),當另外線程需要使用這快資源的時候, 這時候就很可能發生死鎖現象。

上面一段代碼還存在一個隐患,請看下面一小段代碼:

            thread1.Start();  

            Thread.Sleep(1000);  

    當主線程跑(運作)的太快,做完自己的事情去喚醒thread1時,此時thread1還沒有挂起而起喚醒thread1,此時就會出現異常了。并且上面使用的Suspend和Resume方法,編譯器已經出現警告了,提示這兩個方法已經過時, 是以在我們平時使用中應該盡量避免。

1.2 Abort和 Interrupt方法

Abort方法和Interrupt都是用來終止線程的,但是兩者還是有差別的。

1、他們抛出的異常不一樣,Abort 方法抛出的異常是ThreadAbortException, Interrupt抛出的異常為ThreadInterruptedException

2、調用interrupt方法的線程之後可以被喚醒,然而調用Abort方法的線程就直接被終止不能被喚醒的。

下面一段代碼是示範Abort方法的使用

using System;  

using System.Threading;  

namespace ConsoleApplication1  

{  

    class Program  

            Thread abortThread = new Thread(AbortMethod);  

            abortThread.Name = "Abort Thread";  

            abortThread.Start();  

            try 

            {  

                abortThread.Abort();       

            }  

            catch   

                Console.WriteLine("{0} Exception happen in Main Thread", Thread.CurrentThread.Name);  

                Console.WriteLine("{0} Status is:{1} In Main Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);  

            finally 

                Console.WriteLine("{0} Status is:{1} In Main Thread ", abortThread.Name, abortThread.ThreadState);  

            abortThread.Join();  

            Console.WriteLine("{0} Status is:{1} ", abortThread.Name, abortThread.ThreadState);  

        private static void AbortMethod()  

                Thread.Sleep(5000);  

            catch(Exception e)  

                Console.WriteLine(e.GetType().Name);  

                Console.WriteLine("{0} Exception happen In Abort Thread", Thread.CurrentThread.Name);  

                Console.WriteLine("{0} Status is:{1} In Abort Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);  

                Console.WriteLine("{0} Status is:{1} In Abort Thread", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);  

運作結果:

    從運作結果可以看出,調用Abort方法的線程引發的異常類型為ThreadAbortException, 以及異常隻會在 調用Abort方法的線程中發生,而不會在主線程中抛出,并且調用Abort方法後線程的狀态不是立即改變為Aborted狀态,而是從AbortRequested->Aborted。

Interrupt方法:

        { Thread interruptThread = new Thread(AbortMethod);  

            interruptThread.Name = "Interrupt Thread";  

            interruptThread.Start();    

            interruptThread.Interrupt();       

            interruptThread.Join();  

            Console.WriteLine("{0} Status is:{1} ", interruptThread.Name, interruptThread.ThreadState);  

            Console.Read();       

                Console.WriteLine("{0} Exception happen In Interrupt Thread", Thread.CurrentThread.Name);  

                Console.WriteLine("{0} Status is:{1} In Interrupt Thread ", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);  

                Console.WriteLine("{0} Status is:{1} In Interrupt Thread", Thread.CurrentThread.Name, Thread.CurrentThread.ThreadState);  

    }  

 運作結果:

    從結果中可以得到,調用Interrupt方法抛出的異常為:ThreadInterruptException, 以及當調用Interrupt方法後線程的狀态應該是中斷的, 但是從運作結果看此時的線程因為了Join,Sleep方法而喚醒了線程,為了進一步解釋調用Interrupt方法的線程可以被喚醒, 我們可以線上程執行的方法中運用循環,如果線程可以喚醒,則輸出結果中就一定會有循環的部分,然而調用Abort方法線程就直接終止,就不會有循環的部分,下面代碼相信大家看後肯定會更加了解兩個方法的差別的:

namespace ConsoleApplication2  

            Thread thread1 = new Thread(TestMethod);  

            Thread.Sleep(100);  

            thread1.Interrupt();  

            Thread.Sleep(3000);  

            Console.WriteLine("after finnally block, the Thread1 status is:{0}", thread1.ThreadState);  

            for (int i = 0; i < 4; i++)  

                try 

                {  

                    Thread.Sleep(2000);  

                    Console.WriteLine("Thread is Running");  

                }  

                catch (Exception e)  

                    if (e != null)  

                    {  

                        Console.WriteLine("Exception {0} throw ", e.GetType().Name);  

                    }  

                finally 

                    Console.WriteLine("Current Thread status is:{0} ", Thread.CurrentThread.ThreadState);  

運作結果為:

如果把上面的 thread1.Interrupt();改為 thread1.Abort(); 運作結果為:

 二、線程池基礎

    首先,建立和銷毀線程是一個要耗費大量時間的過程,另外,太多的線程也會浪費記憶體資源,是以通過Thread類來建立過多的線程反而有損于性能,為了改善這樣的問題 ,.net中就引入了線程池。

    線程池形象的表示就是存放應用程式中使用的線程的一個集合(就是放線程的地方,這樣線程都放在一個地方就好管理了)。CLR初始化時,線程池中是沒有線程的,在内部, 線程池維護了一個操作請求隊列,當應用程式想執行一個異步操作時,就調用一個方法,就将一個任務放到線程池的隊列中,線程池中代碼從隊列中提取任務,将這個任務委派給一個線程池線程去執行,當線程池線程完成任務時,線程不會被銷毀,而是傳回到線程池中,等待響應另一個請求。由于線程不被銷毀, 這樣就可以避免因為建立線程所産生的性能損失。

注意:通過線程池建立的線程預設為背景線程,優先級預設為Normal.

3.1 建立工作者線程的方法

public static bool QueueUserWorkItem (WaitCallback callBack);

public static bool QueueUserWorkItem(WaitCallback callback, Object state);

這兩個方法向線程池的隊列添加一個工作項(work item)以及一個可選的狀态資料。然後,這兩個方法就會立即傳回。

工作項其實就是由callback參數辨別的一個方法,該方法将由線程池線程執行。同時寫的回調方法必須比對System.Threading.WaitCallback委托類型,定義為:

public delegate void WaitCallback(Object state);

下面示範如何通過線程池線程來實作異步調用:

namespace ThreadPoolUse  

            // 設定線程池中處于活動的線程的最大數目  

            // 設定線程池中工作者線程數量為1000,I/O線程數量為1000  

            ThreadPool.SetMaxThreads(1000, 1000);  

            Console.WriteLine("Main Thread: queue an asynchronous method");  

            PrintMessage("Main Thread Start");  

            // 把工作項添加到隊列中,此時線程池會用工作者線程去執行回調方法  

            ThreadPool.QueueUserWorkItem(asyncMethod);  

        // 方法必須比對WaitCallback委托  

        private static void asyncMethod(object state)  

            PrintMessage("Asynchoronous Method");  

            Console.WriteLine("Asynchoronous thread has worked ");  

        // 列印線程池資訊  

        private static void PrintMessage(String data)  

            int workthreadnumber;  

            int iothreadnumber;  

            // 獲得線程池中可用的線程,把獲得的可用工作者線程數量賦給workthreadnumber變量  

            // 獲得的可用I/O線程數量給iothreadnumber變量  

            ThreadPool.GetAvailableThreads(out workthreadnumber, out iothreadnumber);  

            Console.WriteLine("{0}\n CurrentThreadId is {1}\n CurrentThread is background :{2}\n WorkerThreadNumber is:{3}\n IOThreadNumbers is: {4}\n",  

                data,  

                Thread.CurrentThread.ManagedThreadId,   

                Thread.CurrentThread.IsBackground.ToString(),  

                workthreadnumber.ToString(),  

                iothreadnumber.ToString());  

運作結果:   

從結果中可以看出,線程池中的可用的工作者線程少了一個,用去執行回調方法了。

ThreadPool.QueueUserWorkItem(WaitCallback callback,Object state) 方法可以把object對象作為參數傳送到回調函數中,使用和ThreadPool.QueueUserWorkItem(WaitCallback callback)的使用和類似,這裡就不列出了。

3.2 協作式取消

    .net Framework提供了取消操作的模式, 這個模式是協作式的。為了取消一個操作,首先必須建立一個System.Threading.CancellationTokenSource對象。

下面代碼示範了協作式取消的使用,主要實作當使用者在控制台敲下Enter鍵後就停止數數方法。

using System.Collections.Generic;  

using System.Linq;  

using System.Text;  

namespace ConsoleApplication3  

            Console.WriteLine("Main thread run");      

            PrintMessage("Start");  

            Run();  

            Console.ReadKey();  

        private static void Run()  

            CancellationTokenSource cts = new CancellationTokenSource();  

            // 這裡用Lambda表達式的方式和使用委托的效果一樣的,隻是用了Lambda後可以少定義一個方法。  

            // 這在這裡就是讓大家明白怎麼lambda表達式如何由委托轉變的  

            ////ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000));  

            ThreadPool.QueueUserWorkItem(callback, cts.Token);  

            Console.WriteLine("Press Enter key to cancel the operation\n");  

            Console.ReadLine();  

            // 傳達取消請求  

            cts.Cancel();  

        private static void callback(object state)  

            PrintMessage("Asynchoronous Method Start");  

            CancellationToken token =(CancellationToken)state;      

            Count(token, 1000);  

        // 執行的操作,當受到取消請求時停止數數  

        private static void Count(CancellationToken token,int countto)  

            for (int i = 0; i < countto; i++)  

                if (token.IsCancellationRequested)  

                    Console.WriteLine("Count is canceled");  

                    break;  

                Console.WriteLine(i);  

                Thread.Sleep(300);  

            Console.WriteLine("Cout has done");         

                Thread.CurrentThread.ManagedThreadId,  

 四、使用委托實作異步

    通過調用ThreadPool的QueueUserWorkItem方法來來啟動工作者線程非常友善,但委托WaitCallback指向的是帶有一個參數的無傳回值的方法,如果我們實際操作中需要有傳回值,或者需要帶有多個參數, 這時通過這樣的方式就難以實作, 為了解決這樣的問題,我們可以通過委托來建立工作這線程,

下面代碼示範了使用委托如何實作異步:

namespace Delegate  

        // 使用委托的實作的方式是使用了異步變成模型APM(Asynchronous Programming Model)  

        // 自定義委托  

        private delegate string MyTestdelegate();  

            //執行個體化委托  

            MyTestdelegate testdelegate = new MyTestdelegate(asyncMethod);  

            // 異步調用委托  

            IAsyncResult result = testdelegate.BeginInvoke(null, null);  

            // 擷取結果并列印出來  

            string returndata = testdelegate.EndInvoke(result);  

            Console.WriteLine(returndata);  

        private static string asyncMethod()  

            return "Method has completed";  

 五、任務

同樣 任務的引入也是為了解決通過ThreadPool.QueueUserWorkItem中限制的問題,

下面代碼示範通過任務來實作異步:

5.1 使用任務來實作異步

using System.Threading.Tasks;  

namespace TaskUse  

            // 調用構造函數建立Task對象,  

            Task<int> task = new Task<int>(n => asyncMethod((int)n), 10);  

            // 啟動任務   

            task.Start();  

            // 等待任務完成  

            task.Wait();  

            Console.WriteLine("The Method result is: "+task.Result);  

        private static int asyncMethod(int n)  

            int sum = 0;  

            for (int i = 1; i < n; i++)  

                // 如果n太大,使用checked使下面代碼抛出異常  

                checked 

                    sum += i;  

            return sum;  

5.2 取消任務

如果要取消任務, 同樣可以使用一個CancellationTokenSource對象來取消一個Task.

下面代碼示範了如何來取消一個任務:

            // 調用構造函數建立Task對象,将一個CancellationToken傳給Task構造器進而使Task和CancellationToken關聯起來  

            Task<int> task = new Task<int>(n => asyncMethod(cts.Token, (int)n), 10);  

            // 延遲取消任務  

            // 取消任務  

            Console.WriteLine("The Method result is: " + task.Result);  

        private static int asyncMethod(CancellationToken ct, int n)  

                for (int i = 1; i < n; i++)  

                    // 當CancellationTokenSource對象調用Cancel方法時,  

                    // 就會引起OperationCanceledException異常  

                    // 通過調用CancellationToken的ThrowIfCancellationRequested方法來定時檢查操作是否已經取消,  

                    // 這個方法和CancellationToken的IsCancellationRequested屬性類似  

                    ct.ThrowIfCancellationRequested();  

                    Thread.Sleep(500);  

                    // 如果n太大,使用checked使下面代碼抛出異常  

                    checked 

                        sum += i;  

            catch (Exception e)  

                Console.WriteLine("Exception is:" + e.GetType().Name);  

                Console.WriteLine("Operation is Canceled");  

 5.3 任務工廠

同樣可以通過任務工廠TaskFactory類型來實作異步操作。

namespace TaskFactory  

            Task.Factory.StartNew(() => PrintMessage("Main Thread"));   

 六、小結

    講到這裡CLR的工作者線程大緻講完了,希望也篇文章可以讓大家對線程又有進一步的了解。在後面的一篇線程系列将談談CLR線程池的I/O線程。

     本文轉自LearningHard 51CTO部落格,原文連結:http://blog.51cto.com/learninghard/1034789,如需轉載請自行聯系原作者