原文轉載自:http://kb.cnblogs.com/page/130487/
引言
本文主要從線程的基礎用法,CLR線程池當中工作者線程與I/O線程的開發,並行操作PLINQ等多個方面介紹多線程的開發。
其中委託的BeginInvoke方法以及回調函數最爲常用。
而 I/O線程可能容易遭到大家的忽略,其實在開發多線程系統,更應該多留意I/O線程的操作。特別是在ASP.NET開發當中,可能更多人只會留意在客戶端使用Ajax或者在服務器端使用UpdatePanel。其實合理使用I/O線程在通訊項目或文件下載時,能儘可能地減少IIS的壓力。
並行編程是Framework4.0中極力推廣的異步操作方式,更值得更深入地學習。
希望本篇文章能對各位的學習研究有所幫助,當中有所錯漏的地方敬請點評。
目錄
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線程池系統可以更合理地管理線程的使用。所有請求的服務都能運行於線程池中,當運行結束時線程便會迴歸到線程池。通過設置,能控制線程池的最大線程數量,在請求超出線程最大值時,線程池能按照操作的優先級別來執行,讓部分操作處於等待狀態,待有線程迴歸時再執行操作。
基礎知識就爲大家介紹到這裏,下面將詳細介紹多線程的開發。
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!"); }
運行結果如下
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); } }
運行結果: