細說C#多線程(上)

原文轉載自:http://kb.cnblogs.com/page/130487/

 引言

  本文主要從線程的基礎用法,CLR線程池當中工作者線程與I/O線程的開發,並行操作PLINQ等多個方面介紹多線程的開發。

  其中委託的BeginInvoke方法以及回調函數最爲常用。

  而 I/O線程可能容易遭到大家的忽略,其實在開發多線程系統,更應該多留意I/O線程的操作。特別是在ASP.NET開發當中,可能更多人只會留意在客戶端使用Ajax或者在服務器端使用UpdatePanel。其實合理使用I/O線程在通訊項目或文件下載時,能儘可能地減少IIS的壓力。

  並行編程是Framework4.0中極力推廣的異步操作方式,更值得更深入地學習。

  希望本篇文章能對各位的學習研究有所幫助,當中有所錯漏的地方敬請點評。

  目錄

  一、線程的定義

  二、線程的基礎知識

  三、以ThreadStart方式實現多線程

  四、CLR線程池的工作者線程

  一、線程的定義

   1. 1 進程、應用程序域與線程的關係

  進程(Process)是Windows系統中的一個基本概念,它包含着一個運行程序所需要的資源。進程之間是相對獨立的,一個進程無法訪問另一個進程的數據(除非利用分佈式計算方式),一個進程運行的失敗也不會影響其他進程的運行,Windows系統就是利用進程把工作劃分爲多個獨立的區域的。進程可以理解爲一個程序的基本邊界。

  應用程序域(AppDomain)是一個程序運行的邏輯區域,它可以視爲一個輕量級的進程,.NET的程序集正是在應用程序域中運行的,一個進程可以包含有多個應用程序域,一個應用程序域也可以包含多個程序集。在一個應用程序域中包含了一個或多個上下文context,使用上下文CLR就能夠把某些特殊對象的狀態放置在不同容器當中。

  線程(Thread)是進程中的基本執行單元,在進程入口執行的第一個線程被視爲這個進程的主線程。在.NET應用程序中,都是以Main()方法作爲入口的,當調用此方法時系統就會自動創建一個主線程。線程主要是由CPU寄存器、調用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄當前所執行線程的狀態,調用棧主要用於維護線程所調用到的內存與數據,TLS主要用於存放線程的狀態信息。

  進程、應用程序域、線程的關係如下圖,一個進程內可以包括多個應用程序域,也有包括多個線程,線程也可以穿梭於多個應用程序域當中。但在同一個時刻,線程只會處於一個應用程序域內。 

  由於本文是以介紹多線程技術爲主題,對進程、應用程序域的介紹就到此爲止。關於進程、線程、應用程序域的技術,在“C#綜合揭祕——細說進程線程與應用程序域”會有詳細介紹。

  1. 2 多線程

  在單CPU系統的一個單位時間(time slice)內,CPU只能運行單個線程,運行順序取決於線程的優先級別。如果在單位時間內線程未能完成執行,系統就會把線程的狀態信息保存到線程的本地存儲器(TLS) 中,以便下次執行時恢復執行。而多線程只是系統帶來的一個假像,它在多個單位時間內進行多個線程的切換。因爲切換頻密而且單位時間非常短暫,所以多線程可被視作同時運行。

  適當使用多線程能提高系統的性能,比如:在系統請求大容量的數據時使用多線程,把數據輸出工作交給異步線程,使主線程保持其穩定性去處理其他問題。但需要注意一點,因爲CPU需要花費不少的時間在線程的切換上,所以過多地使用多線程反而會導致性能的下降。

  二、線程的基礎知識

  2. 1 System.Threading.Thread類

  System.Threading.Thread是用於控制線程的基礎類,通過Thread可以控制當前應用程序域中線程的創建、掛起、停止、銷燬。

  它包括以下常用公共屬性:

屬性名稱 說明
CurrentContext 獲取線程正在其中執行的當前上下文。
CurrentThread 獲取當前正在運行的線程。
ExecutionContext 獲取一個 ExecutionContext 對象,該對象包含有關當前線程的各種上下文的信息。
IsAlive 獲取一個值,該值指示當前線程的執行狀態。
IsBackground 獲取或設置一個值,該值指示某個線程是否爲後臺線程。
IsThreadPoolThread 獲取一個值,該值指示線程是否屬於託管線程池。
ManagedThreadId 獲取當前託管線程的唯一標識符。
Name 獲取或設置線程的名稱。
Priority 獲取或設置一個值,該值指示線程的調度優先級。
ThreadState 獲取一個值,該值包含當前線程的狀態。

  2. 1.1 線程的標識符

  ManagedThreadId是確認線程的唯一標識符,程序在大部分情況下都是通過Thread.ManagedThreadId來辨別線程的。而Name是一個可變值,在默認時候,Name爲一個空值 Null,開發人員可以通過程序設置線程的名稱,但這只是一個輔助功能。  

  2. 1.2 線程的優先級別

  .NET爲線程設置了Priority屬性來定義線程執行的優先級別,裏面包含5個選項,其中Normal是默認值。除非系統有特殊要求,否則不應該隨便設置線程的優先級別。  

成員名稱 說明
Lowest 可以將 Thread 安排在具有任何其他優先級的線程之後。
BelowNormal 可以將 Thread 安排在具有 Normal 優先級的線程之後,在具有 Lowest 優先級的線程之前。
Normal 默認選擇。可以將 Thread 安排在具有 AboveNormal 優先級的線程之後,在具有 BelowNormal 優先級的線程之前。
AboveNormal 可以將 Thread 安排在具有 Highest 優先級的線程之後,在具有 Normal 優先級的線程之前。
Highest 可以將 Thread 安排在具有任何其他優先級的線程之前。

  2. 1.3 線程的狀態

  通過ThreadState可以檢測線程是處於Unstarted、Sleeping、Running 等等狀態,它比 IsAlive 屬性能提供更多的特定信息。

  前面說過,一個應用程序域中可能包括多個上下文,而通過CurrentContext可以獲取線程當前的上下文。

  CurrentThread是最常用的一個屬性,它是用於獲取當前運行的線程。  

  2. 1.4 System.Threading.Thread的方法

  Thread 中包括了多個方法來控制線程的創建、掛起、停止、銷燬,以後來的例子中會經常使用。

方法名稱 說明
Abort()     終止本線程。
GetDomain() 返回當前線程正在其中運行的當前域。
GetDomainId() 返回當前線程正在其中運行的當前域Id。
Interrrupt() 中斷處於 WaitSleepJoin 線程狀態的線程。
Join() 已重載。 阻塞調用線程,直到某個線程終止時爲止。
Resume() 繼續運行已掛起的線程。
Start()   執行本線程。
Suspend() 掛起當前線程,如果當前線程已屬於掛起狀態則此不起作用
Sleep()   把正在運行的線程掛起一段時間。

  2. 1.5 開發實例

  以下這個例子,就是通過Thread顯示當前線程信息

static void Main(string[] args)
{
    Thread thread = Thread.CurrentThread;
    thread.Name = "Main Thread";
    string threadMessage = string.Format("Thread ID:{0}\n    Current AppDomainId:{1}\n    "+
        "Current ContextId:{2}\n    Thread Name:{3}\n    "+
        "Thread State:{4}\n    Thread Priority:{5}\n",
        thread.ManagedThreadId, Thread.GetDomainID(), Thread.CurrentContext.ContextID,
        thread.Name, thread.ThreadState, thread.Priority);
    Console.WriteLine(threadMessage);
    Console.ReadKey();
}

  運行結果

  2. 2  System.Threading 命名空間

  在System.Threading命名空間內提供多個方法來構建多線程應用程序,其中ThreadPool與Thread是多線程開發中最常用到的,在.NET中專門設定了一個CLR線程池專門用於管理線程的運行,這個CLR線程池正是通過ThreadPool類來管理。而Thread是管理線程的最直接方式,下面幾節將詳細介紹有關內容。

類     說明
AutoResetEvent 通知正在等待的線程已發生事件。無法繼承此類。
ExecutionContext 管理當前線程的執行上下文。無法繼承此類。
Interlocked 爲多個線程共享的變量提供原子操作。
Monitor 提供同步對對象的訪問的機制。
Mutex 一個同步基元,也可用於進程間同步。
Thread 創建並控制線程,設置其優先級並獲取其狀態。
ThreadAbortException 在對 Abort 方法進行調用時引發的異常。無法繼承此類。
ThreadPool 提供一個線程池,該線程池可用於發送工作項、處理異步 I/O、代表其他線程等待以及處理計時器。
Timeout 包含用於指定無限長的時間的常數。無法繼承此類。
Timer 提供以指定的時間間隔執行方法的機制。無法繼承此類。
WaitHandle 封裝等待對共享資源的獨佔訪問的操作系統特定的對象。

  在System.Threading中的包含了下表中的多個常用委託,其中ThreadStart、ParameterizedThreadStart是最常用到的委託。

  由ThreadStart生成的線程是最直接的方式,但由ThreadStart所生成並不受線程池管理。

  而ParameterizedThreadStart是爲異步觸發帶參數的方法而設的,在下一節將爲大家逐一細說。

委託說明
ContextCallback 表示要在新上下文中調用的方法。
ParameterizedThreadStart 表示在 Thread 上執行的方法。
ThreadExceptionEventHandler 表示將要處理 Application 的 ThreadException 事件的方法。
ThreadStart 表示在 Thread 上執行的方法。
TimerCallback 表示處理來自 Timer 的調用的方法。
WaitCallback 表示線程池線程要執行的回調方法。
WaitOrTimerCallback 表示當 WaitHandle 超時或終止時要調用的方法。

  2. 3 線程的管理方式

  通過ThreadStart來創建一個新線程是最直接的方法,但這樣創建出來的線程比較難管理,如果創建過多的線程反而會讓系統的性能下載。有見及此,.NET爲線程管理專門設置了一個CLR線程池,使用CLR線程池系統可以更合理地管理線程的使用。所有請求的服務都能運行於線程池中,當運行結束時線程便會迴歸到線程池。通過設置,能控制線程池的最大線程數量,在請求超出線程最大值時,線程池能按照操作的優先級別來執行,讓部分操作處於等待狀態,待有線程迴歸時再執行操作。

  基礎知識就爲大家介紹到這裏,下面將詳細介紹多線程的開發。

  三、以ThreadStart方式實現多線程

  3. 1 使用ThreadStart委託

  這裏先以一個例子體現一下多線程帶來的好處,首先在Message類中建立一個方法ShowMessage(),裏面顯示了當前運行線程的Id,並使用Thread.Sleep(int ) 方法模擬部分工作。在main()中通過ThreadStart委託綁定Message對象的ShowMessage()方法,然後通過Thread.Start()執行異步方法。

public class Message
{
    public void ShowMessage()
    {
        string message = string.Format("Async threadId is :{0}",
                                        Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);

        for (int n = 0; n < 10; n++)
        {
            Thread.Sleep(300);   
            Console.WriteLine("The number is:" + n.ToString()); 
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Main threadId is:"+
                          Thread.CurrentThread.ManagedThreadId);
        Message message=new Message();
        Thread thread = new Thread(new ThreadStart(message.ShowMessage));
        thread.Start();
        Console.WriteLine("Do something ..........!");
        Console.WriteLine("Main thread working is complete!");
        
    }
}

  請注意運行結果,在調用Thread.Start()方法後,系統以異步方式運行Message.ShowMessage(),而主線程的操作是繼續執行的,在Message.ShowMessage()完成前,主線程已完成所有的操作。

  3. 2 使用ParameterizedThreadStart委託

  ParameterizedThreadStart委託與ThreadStart委託非常相似,但ParameterizedThreadStart委託是面向帶參數方法的。注意ParameterizedThreadStart 對應方法的參數爲object,此參數可以爲一個值對象,也可以爲一個自定義對象。

public class Person
{
    public string Name
    {
        get;
        set;
    }
    public int Age
    {
        get;
        set;
    }
}

public class Message
{
    public void ShowMessage(object person)
    {
        if (person != null)
        {
            Person _person = (Person)person;
            string message = string.Format("\n{0}'s age is {1}!\nAsync threadId is:{2}",
                _person.Name,_person.Age,Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine(message);
        }
        for (int n = 0; n < 10; n++)
        {
            Thread.Sleep(300);   
            Console.WriteLine("The number is:" + n.ToString()); 
        }
    }
}

class Program
{
    static void Main(string[] args)
    {     
        Console.WriteLine("Main threadId is:"+Thread.CurrentThread.ManagedThreadId);
        
        Message message=new Message();
        //綁定帶參數的異步方法
        Thread thread = new Thread(new ParameterizedThreadStart(message.ShowMessage));
        Person person = new Person();
        person.Name = "Jack";
        person.Age = 21;
        thread.Start(person);  //啓動異步線程 
        
        Console.WriteLine("Do something ..........!");
        Console.WriteLine("Main thread working is complete!");
         
    }
}

  運行結果:

  3. 3 前臺線程與後臺線程

  注意以上兩個例子都沒有使用Console.ReadKey(),但系統依然會等待異步線程完成後纔會結束。這是因爲使用Thread.Start()啓動的線程默認爲前臺線程,而系統必須等待所有前臺線程運行結束後,應用程序域纔會自動卸載。

  在第二節曾經介紹過線程Thread有一個屬性IsBackground,通過把此屬性設置爲true,就可以把線程設置爲後臺線程!這時應用程序域將在主線程完成時就被卸載,而不會等待異步線程的運行。

  3. 4 掛起線程

  爲了等待其他後臺線程完成後再結束主線程,就可以使用Thread.Sleep()方法。

public class Message
{
    public void ShowMessage()
    {
        string message = string.Format("\nAsync threadId is:{0}",
                                       Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
        for (int n = 0; n < 10; n++)
        {
            Thread.Sleep(300);
            Console.WriteLine("The number is:" + n.ToString());
        }
    }
}

class Program
{
    static void Main(string[] args)
    {     
        Console.WriteLine("Main threadId is:"+
                          Thread.CurrentThread.ManagedThreadId);
        
        Message message=new Message();
        Thread thread = new Thread(new ThreadStart(message.ShowMessage));
        thread.IsBackground = true;
        thread.Start();
        
        Console.WriteLine("Do something ..........!");
        Console.WriteLine("Main thread working is complete!");
        Console.WriteLine("Main thread sleep!");
        Thread.Sleep(5000);
    }
}

  運行結果如下,此時應用程序域將在主線程運行5秒後自動結束

  但系統無法預知異步線程需要運行的時間,所以用通過Thread.Sleep(int)阻塞主線程並不是一個好的解決方法。有見及此,.NET專門爲等待異步線程完成開發了另一個方法thread.Join()。把上面例子中的最後一行Thread.Sleep(5000)修改爲 thread.Join() 就能保證主線程在異步線程thread運行結束後纔會終止。

  3. 5 Suspend 與 Resume (慎用)

  Thread.Suspend()與 Thread.Resume()是在Framework1.0 就已經存在的老方法了,它們分別可以掛起、恢復線程。但在Framework2.0中就已經明確排斥這兩個方法。這是因爲一旦某個線程佔用了已有的資源,再使用Suspend()使線程長期處於掛起狀態,當在其他線程調用這些資源的時候就會引起死鎖!所以在沒有必要的情況下應該避免使用這兩個方法。

  3. 6 終止線程

  若想終止正在運行的線程,可以使用Abort()方法。在使用Abort()的時候,將引發一個特殊異常 ThreadAbortException 。

  若想在線程終止前恢復線程的執行,可以在捕獲異常後 ,在catch(ThreadAbortException ex){...} 中調用Thread.ResetAbort()取消終止。

  而使用Thread.Join()可以保證應用程序域等待異步線程結束後才終止運行。

static void Main(string[] args)
{
    Console.WriteLine("Main threadId is:" +
                      Thread.CurrentThread.ManagedThreadId);

    Thread thread = new Thread(new ThreadStart(AsyncThread));
    thread.IsBackground = true;
    thread.Start();
    thread.Join();

}     

//以異步方式調用
static void AsyncThread()
{
    try
    {
        string message = string.Format("\nAsync threadId is:{0}",
           Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);

        for (int n = 0; n < 10; n++)
        {
            //當n等於4時,終止線程
            if (n >= 4)
            {
                Thread.CurrentThread.Abort(n);
            }
            Thread.Sleep(300);
            Console.WriteLine("The number is:" + n.ToString());
        }
    }
    catch (ThreadAbortException ex)
    {
        //輸出終止線程時n的值
        if (ex.ExceptionState != null)
            Console.WriteLine(string.Format("Thread abort when the number is: {0}!", 
                                             ex.ExceptionState.ToString()));
       
        //取消終止,繼續執行線程
        Thread.ResetAbort();
        Console.WriteLine("Thread ResetAbort!");
    }

    //線程結束
    Console.WriteLine("Thread Close!");
}

  運行結果如下

  四、CLR線程池的工作者線程

  4. 1 關於CLR線程池

  使用ThreadStart與ParameterizedThreadStart建立新線程非常簡單,但通過此方法建立的線程難於管理,若建立過多的線程反而會影響系統的性能。

  有見及此,.NET引入CLR線程池這個概念。CLR線程池並不會在CLR初始化的時候立刻建立線程,而是在應用程序要創建線程來執行任務時,線程池才初始化一個線程。線程的初始化與其他的線程一樣。在完成任務以後,該線程不會自行銷燬,而是以掛起的狀態返回到線程池。直到應用程序再次向線程池發出請求時,線程池裏掛起的線程就會再度激活執行任務。這樣既節省了建立線程所造成的性能損耗,也可以讓多個任務反覆重用同一線程,從而在應用程序生存期內節約大量開銷。

  注意通過CLR線程池所建立的線程總是默認爲後臺線程,優先級數爲ThreadPriority.Normal。

  4. 2 工作者線程與I/O線程

  CLR線程池分爲工作者線程(workerThreads)與I/O線程 (completionPortThreads) 兩種,工作者線程是主要用作管理CLR內部對象的運作,I/O(Input/Output) 線程顧名思義是用於與外部系統交換信息,IO線程的細節將在下一節詳細說明。

  通過ThreadPool.GetMax(out int workerThreads,out int completionPortThreads )和 ThreadPool.SetMax( int workerThreads, int completionPortThreads)兩個方法可以分別讀取和設置CLR線程池中工作者線程與I/O線程的最大線程數。在Framework2.0中最大線程默認爲25*CPU數,在Framewok3.0、4.0中最大線程數默認爲250*CPU數,在近年 I3,I5,I7 CPU出現後,線程池的最大值一般默認爲1000、2000。

  若想測試線程池中有多少的線程正在投入使用,可以通過ThreadPool.GetAvailableThreads( out int workerThreads,out int completionPortThreads ) 方法。

  使用CLR線程池的工作者線程一般有兩種方式,一是直接通過 ThreadPool.QueueUserWorkItem() 方法,二是通過委託,下面將逐一細說。

  4. 3 通過QueueUserWorkItem啓動工作者線程

  ThreadPool線程池中包含有兩個靜態方法可以直接啓動工作者線程:

  一爲 ThreadPool.QueueUserWorkItem(WaitCallback)

  二爲 ThreadPool.QueueUserWorkItem(WaitCallback,Object) 

  先把WaitCallback委託指向一個帶有Object參數的無返回值方法,再使用 ThreadPool.QueueUserWorkItem(WaitCallback) 就可以異步啓動此方法,此時異步方法的參數被視爲null 。

class Program
{
    static void Main(string[] args)
    {
        //把CLR線程池的最大值設置爲1000
        ThreadPool.SetMaxThreads(1000, 1000);
        //顯示主線程啓動時線程池信息
        ThreadMessage("Start");
        //啓動工作者線程
        ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback));
        Console.ReadKey();
    }
    
    static void AsyncCallback(object state)
    {
        Thread.Sleep(200);
        ThreadMessage("AsyncCallback");
        Console.WriteLine("Async thread do work!");
    }

    //顯示線程現狀
    static void ThreadMessage(string data)
    {
        string message = string.Format("{0}\n  CurrentThreadId is {1}",
             data, Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }
}

  運行結果

  使用 ThreadPool.QueueUserWorkItem(WaitCallback,Object) 方法可以把object對象作爲參數傳送到回調函數中。

  下面例子中就是把一個string對象作爲參數發送到回調函數當中。

class Program
{
    static void Main(string[] args)
    {
        //把線程池的最大值設置爲1000
        ThreadPool.SetMaxThreads(1000, 1000);
      
        ThreadMessage("Start");
        ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback),"Hello Elva");
        Console.ReadKey();
    }

    static void AsyncCallback(object state)
    {
        Thread.Sleep(200);
        ThreadMessage("AsyncCallback");

        string data = (string)state;
        Console.WriteLine("Async thread do work!\n"+data);
    }

    //顯示線程現狀
    static void ThreadMessage(string data)
    {
        string message = string.Format("{0}\n  CurrentThreadId is {1}",
             data, Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }
}

  運行結果

  通過ThreadPool.QueueUserWorkItem啓動工作者線程雖然是方便,但WaitCallback委託指向的必須是一個帶有Object參數的無返回值方法,這無疑是一種限制。若方法需要有返回值,或者帶有多個參數,這將多費周折。有見及此,.NET提供了另一種方式去建立工作者線程,那就是委託。

  4. 4  委託類       

  使用CLR線程池中的工作者線程,最靈活最常用的方式就是使用委託的異步方法,在此先簡單介紹一下委託類。

  當定義委託後,.NET就會自動創建一個代表該委託的類,下面可以用反射方式顯示委託類的方法成員(對反射有興趣的朋友可以先參考一下“.NET基礎篇——反射的奧妙”)

class Program
{
   delegate void MyDelegate();

   static void Main(string[] args)
   {
       MyDelegate delegate1 = new MyDelegate(AsyncThread);
       //顯示委託類的幾個方法成員     
       var methods=delegate1.GetType().GetMethods();
       if (methods != null)
           foreach (MethodInfo info in methods)
               Console.WriteLine(info.Name);
       Console.ReadKey();
    }
} 

  委託類包括以下幾個重要方法

public class MyDelegate:MulticastDelegate
{
    public MyDelegate(object target, int methodPtr);
    //調用委託方法
    public virtual void Invoke();
    //異步委託
    public virtual IAsyncResult BeginInvoke(AsyncCallback callback,object state);
    public virtual void EndInvoke(IAsyncResult result);
}

  當調用Invoke()方法時,對應此委託的所有方法都會被執行。而BeginInvoke與EndInvoke則支持委託方法的異步調用,由BeginInvoke啓動的線程都屬於CLR線程池中的工作者線程,在下面將詳細說明。

  4. 5  利用BeginInvoke與EndInvoke完成異步委託方法

  首先建立一個委託對象,通過IAsyncResult BeginInvoke(string name,AsyncCallback callback,object state) 異步調用委託方法,BeginInvoke 方法除最後的兩個參數外,其它參數都是與方法參數相對應的。通過 BeginInvoke 方法將返回一個實現了 System.IAsyncResult 接口的對象,之後就可以利用EndInvoke(IAsyncResult ) 方法就可以結束異步操作,獲取委託的運行結果。

class Program
{
    delegate string MyDelegate(string name);

    static void Main(string[] args)
    {
        ThreadMessage("Main Thread");
        
        //建立委託
        MyDelegate myDelegate = new MyDelegate(Hello);
        //異步調用委託,獲取計算結果
        IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null);
        //完成主線程其他工作
        ............. 
        //等待異步方法完成,調用EndInvoke(IAsyncResult)獲取運行結果
        string data=myDelegate.EndInvoke(result);
        Console.WriteLine(data);
        
        Console.ReadKey();
    }

    static string Hello(string name)
    {
        ThreadMessage("Async Thread");
        Thread.Sleep(2000);            //虛擬異步工作
        return "Hello " + name;
    }

    //顯示當前線程
    static void ThreadMessage(string data)
    {
        string message = string.Format("{0}\n  ThreadId is:{1}",
               data,Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }
}

  運行結果

  4. 6  善用IAsyncResult

  在以上例子中可以看見,如果在使用myDelegate.BeginInvoke後立即調用myDelegate.EndInvoke,那在異步線程未完成工作以前主線程將處於阻塞狀態,等到異步線程結束獲取計算結果後,主線程才能繼續工作,這明顯無法展示出多線程的優勢。此時可以好好利用IAsyncResult 提高主線程的工作性能,IAsyncResult有以下成員:

public interface IAsyncResult
{
    object AsyncState {get;}            //獲取用戶定義的對象,它限定或包含關於異步操作的信息。
    WailHandle AsyncWaitHandle {get;}   //獲取用於等待異步操作完成的 WaitHandle。
    bool CompletedSynchronously {get;}  //獲取異步操作是否同步完成的指示。
    bool IsCompleted {get;}             //獲取異步操作是否已完成的指示。
}

  通過輪詢方式,使用IsCompleted屬性判斷異步操作是否完成,這樣在異步操作未完成前就可以讓主線程執行另外的工作。

class Program
{
    delegate string MyDelegate(string name);

    static void Main(string[] args)
    {
        ThreadMessage("Main Thread");
        
        //建立委託
        MyDelegate myDelegate = new MyDelegate(Hello);
        //異步調用委託,獲取計算結果
        IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null);
        //在異步線程未完成前執行其他工作
        while (!result.IsCompleted)
        {
            Thread.Sleep(200);      //虛擬操作
            Console.WriteLine("Main thead do work!");
        }
        string data=myDelegate.EndInvoke(result);
        Console.WriteLine(data);
        
        Console.ReadKey();
    }

    static string Hello(string name)
    {
        ThreadMessage("Async Thread");
        Thread.Sleep(2000);
        return "Hello " + name;
    }

    static void ThreadMessage(string data)
    {
        string message = string.Format("{0}\n  ThreadId is:{1}",
               data,Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }
}

  運行結果:

  除此以外,也可以使用WailHandle完成同樣的工作,WaitHandle裏面包含有一個方法WaitOne(int timeout),它可以判斷委託是否完成工作,在工作未完成前主線程可以繼續其他工作。運行下面代碼可得到與使用 IAsyncResult.IsCompleted 同樣的結果,而且更簡單方便 。

namespace Test
{
    class Program
    {
        delegate string MyDelegate(string name);

        static void Main(string[] args)
        {
            ThreadMessage("Main Thread");
            
            //建立委託
            MyDelegate myDelegate = new MyDelegate(Hello);
 
            //異步調用委託,獲取計算結果
            IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null);
            
            while (!result.AsyncWaitHandle.WaitOne(200))
            {
                Console.WriteLine("Main thead do work!");
            }
            string data=myDelegate.EndInvoke(result);
            Console.WriteLine(data);
            
            Console.ReadKey();
        }

        static string Hello(string name)
        {
            ThreadMessage("Async Thread");
            Thread.Sleep(2000);
            return "Hello " + name;
        }

        static void ThreadMessage(string data)
        {
            string message = string.Format("{0}\n  ThreadId is:{1}",
                   data,Thread.CurrentThread.ManagedThreadId);
            Console.WriteLine(message);
        }
    }

  當要監視多個運行對象的時候,使用IAsyncResult.WaitHandle.WaitOne可就派不上用場了。

  幸好.NET爲WaitHandle準備了另外兩個靜態方法:WaitAny(waitHandle[], int)與WaitAll (waitHandle[] , int)。

  其中WaitAll在等待所有waitHandle完成後再返回一個bool值。

  而WaitAny是等待其中一個waitHandle完成後就返回一個int,這個int是代表已完成waitHandle在waitHandle[]中的數組索引。

  下面就是使用WaitAll的例子,運行結果與使用 IAsyncResult.IsCompleted 相同。

class Program
{
    delegate string MyDelegate(string name);

    static void Main(string[] args)
    {
        ThreadMessage("Main Thread");
        
        //建立委託
        MyDelegate myDelegate = new MyDelegate(Hello);

        //異步調用委託,獲取計算結果
        IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null);

        //此處可加入多個檢測對象
        WaitHandle[] waitHandleList = new WaitHandle[] { result.AsyncWaitHandle,........ };
        while (!WaitHandle.WaitAll(waitHandleList,200))
        {
            Console.WriteLine("Main thead do work!");
        }
        string data=myDelegate.EndInvoke(result);
        Console.WriteLine(data);
        
        Console.ReadKey();
    }

    static string Hello(string name)
    {
        ThreadMessage("Async Thread");
        Thread.Sleep(2000);
        return "Hello " + name;
    }

    static void ThreadMessage(string data)
    {
        string message = string.Format("{0}\n  ThreadId is:{1}",
               data,Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }
}

  4. 7 回調函數

  使用輪詢方式來檢測異步方法的狀態非常麻煩,而且效率不高,有見及此,.NET爲 IAsyncResult BeginInvoke(AsyncCallback , object)準備了一個回調函數。使用 AsyncCallback 就可以綁定一個方法作爲回調函數,回調函數必須是帶參數 IAsyncResult 且無返回值的方法: void AsycnCallbackMethod(IAsyncResult result) 。在BeginInvoke方法完成後,系統就會調用AsyncCallback所綁定的回調函數,最後回調函數中調用 XXX EndInvoke(IAsyncResult result) 就可以結束異步方法,它的返回值類型與委託的返回值一致。

class Program
{
    delegate string MyDelegate(string name);

    static void Main(string[] args)
    {
        ThreadMessage("Main Thread");

        //建立委託
        MyDelegate myDelegate = new MyDelegate(Hello);
        //異步調用委託,獲取計算結果
        myDelegate.BeginInvoke("Leslie", new AsyncCallback(Completed), null);
        //在啓動異步線程後,主線程可以繼續工作而不需要等待
        for (int n = 0; n < 6; n++)
            Console.WriteLine("  Main thread do work!");
        Console.WriteLine("");

        Console.ReadKey();
    }

    static string Hello(string name)
    {
        ThreadMessage("Async Thread");
        Thread.Sleep(2000);             \\模擬異步操作
        return "\nHello " + name;
    }

    static void Completed(IAsyncResult result)
    {
        ThreadMessage("Async Completed");

        //獲取委託對象,調用EndInvoke方法獲取運行結果
        AsyncResult _result = (AsyncResult)result;
        MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate;
        string data = myDelegate.EndInvoke(_result);
        Console.WriteLine(data);
    }

    static void ThreadMessage(string data)
    {
        string message = string.Format("{0}\n  ThreadId is:{1}",
               data, Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }
}

  可以看到,主線在調用BeginInvoke方法可以繼續執行其他命令,而無需再等待了,這無疑比使用輪詢方式判斷異步方法是否完成更有優勢。

  在異步方法執行完成後將會調用AsyncCallback所綁定的回調函數,注意一點,回調函數依然是在異步線程中執行,這樣就不會影響主線程的運行,這也使用回調函數最值得青昧的地方。

  在回調函數中有一個既定的參數IAsyncResult,把IAsyncResult強制轉換爲AsyncResult後,就可以通過 AsyncResult.AsyncDelegate 獲取原委託,再使用EndInvoke方法獲取計算結果。

  運行結果如下:

  如果想爲回調函數傳送一些外部信息,就可以利用BeginInvoke(AsyncCallback,object)的最後一個參數object,它允許外部向回調函數輸入任何類型的參數。只需要在回調函數中利用 AsyncResult.AsyncState 就可以獲取object對象。

class Program
{
    public class Person
    {
        public string Name;
        public int Age;
    }

    delegate string MyDelegate(string name);

    static void Main(string[] args)
    {
        ThreadMessage("Main Thread");

        //建立委託
        MyDelegate myDelegate = new MyDelegate(Hello);
        
        //建立Person對象
        Person person = new Person();
        person.Name = "Elva";
        person.Age = 27;
        
        //異步調用委託,輸入參數對象person, 獲取計算結果
        myDelegate.BeginInvoke("Leslie", new AsyncCallback(Completed), person);            
      
        //在啓動異步線程後,主線程可以繼續工作而不需要等待
        for (int n = 0; n < 6; n++)
            Console.WriteLine("  Main thread do work!");
        Console.WriteLine("");

        Console.ReadKey();
    }

    static string Hello(string name)
    {
        ThreadMessage("Async Thread");
        Thread.Sleep(2000);
        return "\nHello " + name;
    }

    static void Completed(IAsyncResult result)
    {
        ThreadMessage("Async Completed");

        //獲取委託對象,調用EndInvoke方法獲取運行結果
        AsyncResult _result = (AsyncResult)result;
        MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate;
        string data = myDelegate.EndInvoke(_result);
        //獲取Person對象
        Person person = (Person)result.AsyncState;
        string message = person.Name + "'s age is " + person.Age.ToString();

        Console.WriteLine(data+"\n"+message);
    }

    static void ThreadMessage(string data)
    {
        string message = string.Format("{0}\n  ThreadId is:{1}",
               data, Thread.CurrentThread.ManagedThreadId);
        Console.WriteLine(message);
    }
}

  運行結果:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章