一、線程的定義
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可以控制當前應用程序域中線程的創建、掛起、停止、銷毀。
它包括以下常用公共屬性:
2.1.1 線程的標識符
ManagedThreadId是確認線程的唯一標識符,程序在大部分情況下都是通過Thread.ManagedThreadId來辨別線程的。而Name是一個可變值,在默認時候,Name為一個空值 Null,開發人員可以通過程序設置線程的名稱,但這只是一個輔助功能。
2.1.2 線程的優先級別
.NET為線程設置了Priority屬性來定義線程執行的優先級別,裡面包含5個選項,其中Normal是默認值。除非系統有特殊要求,否則不應該隨便設置線程的優先級別。
2.1.3 線程的狀態
通過ThreadState可以檢測線程是處於Unstarted、Sleeping、Running 等等狀態,它比 IsAlive 屬性能提供更多的特定信息。
前面說過,一個應用程序域中可能包括多個上下文,而通過CurrentContext可以獲取線程當前的上下文。
CurrentThread是最常用的一個屬性,它是用於獲取當前運行的線程。
2.1.4 System.Threading.Thread的方法
Thread 中包括了多個方法來控制線程的創建、掛起、停止、銷毀,以後來的例子中會經常使用。
2.1.5 開發實例
以下這個例子,就是通過Thread顯示當前線程信息
1 static void Main(string[] args) 2 { 3 Thread thread = Thread.CurrentThread; 4 thread.Name = "Main Thread"; 5 string threadMessage = string.Format("Thread ID:{0}\n Current AppDomainId:{1}\n "+ 6 "Current ContextId:{2}\n Thread Name:{3}\n "+ 7 "Thread State:{4}\n Thread Priority:{5}\n", 8 thread.ManagedThreadId, Thread.GetDomainID(), Thread.CurrentContext.ContextID, 9 thread.Name, thread.ThreadState, thread.Priority); 10 Console.WriteLine(threadMessage); 11 Console.ReadKey(); 12 }
運行結果
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是為異步觸發帶參數的方法而設的,在下一節將為大家逐一細說。
2.3 線程的管理方式
通過ThreadStart來創建一個新線程是最直接的方法,但這樣創建出來的線程比較難管理,如果創建過多的線程反而會讓系統的性能下載。有見及此,.NET為線程管理專門設置了一個CLR線程池,使用CLR線程池系統可以更合理地管理線程的使用。所有請求的服務都能運行於線程池中,當運行結束時線程便會回歸到線程池。通過設置,能控制線程池的最大線程數量,在請求超出線程最大值時,線程池能按照操作的優先級別來執行,讓部分操作處於等待狀態,待有線程回歸時再執行操作。
基礎知識就為大家介紹到這裡,下面將詳細介紹多線程的開發。
返回目錄
三、以ThreadStart方式實現多線程
3.1 使用ThreadStart委托
這裡先以一個例子體現一下多線程帶來的好處,首先在Message類中建立一個方法ShowMessage(),裡面顯示了當前運行線程的Id,並使用Thread.Sleep(int ) 方法模擬部分工作。在main()中通過ThreadStart委托綁定Message對象的ShowMessage()方法,然後通過Thread.Start()執行異步方法。
1 public class Message 2 { 3 public void ShowMessage() 4 { 5 string message = string.Format("Async threadId is :{0}", 6 Thread.CurrentThread.ManagedThreadId); 7 Console.WriteLine(message); 8 9 for (int n = 0; n < 10; n++) 10 { 11 Thread.Sleep(300); 12 Console.WriteLine("The number is:" + n.ToString()); 13 } 14 } 15 } 16 17 class Program 18 { 19 static void Main(string[] args) 20 { 21 Console.WriteLine("Main threadId is:"+ 22 Thread.CurrentThread.ManagedThreadId); 23 Message message=new Message(); 24 Thread thread = new Thread(new ThreadStart(message.ShowMessage)); 25 thread.Start(); 26 Console.WriteLine("Do something ..........!"); 27 Console.WriteLine("Main thread working is complete!"); 28 29 } 30 }
請注意運行結果,在調用Thread.Start()方法後,系統以異步方式運行Message.ShowMessage(),而主線程的操作是繼續執行的,在Message.ShowMessage()完成前,主線程已完成所有的操作。
3.2 使用ParameterizedThreadStart委托
ParameterizedThreadStart委托與ThreadStart委托非常相似,但ParameterizedThreadStart委托是面向帶參數方法的。注意ParameterizedThreadStart 對應方法的參數為object,此參數可以為一個值對象,也可以為一個自定義對象。
1 public class Person 2 { 3 public string Name 4 { 5 get; 6 set; 7 } 8 public int Age 9 { 10 get; 11 set; 12 } 13 } 14 15 public class Message 16 { 17 public void ShowMessage(object person) 18 { 19 if (person != null) 20 { 21 Person _person = (Person)person; 22 string message = string.Format("\n{0}'s age is {1}!\nAsync threadId is:{2}", 23 _person.Name,_person.Age,Thread.CurrentThread.ManagedThreadId); 24 Console.WriteLine(message); 25 } 26 for (int n = 0; n < 10; n++) 27 { 28 Thread.Sleep(300); 29 Console.WriteLine("The number is:" + n.ToString()); 30 } 31 } 32 } 33 34 class Program 35 { 36 static void Main(string[] args) 37 { 38 Console.WriteLine("Main threadId is:"+Thread.CurrentThread.ManagedThreadId); 39 40 Message message=new Message(); 41 //綁定帶參數的異步方法 42 Thread thread = new Thread(new ParameterizedThreadStart(message.ShowMessage)); 43 Person person = new Person(); 44 person.Name = "Jack"; 45 person.Age = 21; 46 thread.Start(person); //啟動異步線程 47 48 Console.WriteLine("Do something ..........!"); 49 Console.WriteLine("Main thread working is complete!"); 50 51 } 52 }
運行結果:
3.3 前台線程與後台線程
注意以上兩個例子都沒有使用Console.ReadKey(),但系統依然會等待異步線程完成後才會結束。這是因為使用Thread.Start()啟動的線程默認為前台線程,而系統必須等待所有前台線程運行結束後,應用程序域才會自動卸載。
在第二節曾經介紹過線程Thread有一個屬性IsBackground,通過把此屬性設置為true,就可以把線程設置為後台線程!這時應用程序域將在主線程完成時就被卸載,而不會等待異步線程的運行。
3.4 掛起線程
為了等待其他後台線程完成後再結束主線程,就可以使用Thread.Sleep()方法。
1 public class Message 2 { 3 public void ShowMessage() 4 { 5 string message = string.Format("\nAsync threadId is:{0}", 6 Thread.CurrentThread.ManagedThreadId); 7 Console.WriteLine(message); 8 for (int n = 0; n < 10; n++) 9 { 10 Thread.Sleep(300); 11 Console.WriteLine("The number is:" + n.ToString()); 12 } 13 } 14 } 15 16 class Program 17 { 18 static void Main(string[] args) 19 { 20 Console.WriteLine("Main threadId is:"+ 21 Thread.CurrentThread.ManagedThreadId); 22 23 Message message=new Message(); 24 Thread thread = new Thread(new ThreadStart(message.ShowMessage)); 25 thread.IsBackground = true; 26 thread.Start(); 27 28 Console.WriteLine("Do something ..........!"); 29 Console.WriteLine("Main thread working is complete!"); 30 Console.WriteLine("Main thread sleep!"); 31 Thread.Sleep(5000); 32 } 33 }
運行結果如下,此時應用程序域將在主線程運行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()可以保證應用程序域等待異步線程結束後才終止運行。
1 static void Main(string[] args) 2 { 3 Console.WriteLine("Main threadId is:" + 4 Thread.CurrentThread.ManagedThreadId); 5 6 Thread thread = new Thread(new ThreadStart(AsyncThread)); 7 thread.IsBackground = true; 8 thread.Start(); 9 thread.Join(); 10 11 } 12 13 //以異步方式調用 14 static void AsyncThread() 15 { 16 try 17 { 18 string message = string.Format("\nAsync threadId is:{0}", 19 Thread.CurrentThread.ManagedThreadId); 20 Console.WriteLine(message); 21 22 for (int n = 0; n < 10; n++) 23 { 24 //當n等於4時,終止線程 25 if (n >= 4) 26 { 27 Thread.CurrentThread.Abort(n); 28 } 29 Thread.Sleep(300); 30 Console.WriteLine("The number is:" + n.ToString()); 31 } 32 } 33 catch (ThreadAbortException ex) 34 { 35 //輸出終止線程時n的值 36 if (ex.ExceptionState != null) 37 Console.WriteLine(string.Format("Thread abort when the number is: {0}!", 38 ex.ExceptionState.ToString())); 39 40 //取消終止,繼續執行線程 41 Thread.ResetAbort(); 42 Console.WriteLine("Thread ResetAbort!"); 43 } 44 45 //線程結束 46 Console.WriteLine("Thread Close!"); 47 }
運行結果如下
返回目錄
四、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 。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //把CLR線程池的最大值設置為1000 6 ThreadPool.SetMaxThreads(1000, 1000); 7 //顯示主線程啟動時線程池信息 8 ThreadMessage("Start"); 9 //啟動工作者線程 10 ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback)); 11 Console.ReadKey(); 12 } 13 14 static void AsyncCallback(object state) 15 { 16 Thread.Sleep(200); 17 ThreadMessage("AsyncCallback"); 18 Console.WriteLine("Async thread do work!"); 19 } 20 21 //顯示線程現狀 22 static void ThreadMessage(string data) 23 { 24 string message = string.Format("{0}\n CurrentThreadId is {1}", 25 data, Thread.CurrentThread.ManagedThreadId); 26 Console.WriteLine(message); 27 } 28 }
運行結果
使用 ThreadPool.QueueUserWorkItem(WaitCallback,Object) 方法可以把object對象作為參數傳送到回調函數中。 下面例子中就是把一個string對象作為參數發送到回調函數當中。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 //把線程池的最大值設置為1000 6 ThreadPool.SetMaxThreads(1000, 1000); 7 8 ThreadMessage("Start"); 9 ThreadPool.QueueUserWorkItem(new WaitCallback(AsyncCallback),"Hello Elva"); 10 Console.ReadKey(); 11 } 12 13 static void AsyncCallback(object state) 14 { 15 Thread.Sleep(200); 16 ThreadMessage("AsyncCallback"); 17 18 string data = (string)state; 19 Console.WriteLine("Async thread do work!\n"+data); 20 } 21 22 //顯示線程現狀 23 static void ThreadMessage(string data) 24 { 25 string message = string.Format("{0}\n CurrentThreadId is {1}", 26 data, Thread.CurrentThread.ManagedThreadId); 27 Console.WriteLine(message); 28 } 29 }
運行結果
通過ThreadPool.QueueUserWorkItem啟動工作者線程雖然是方便,但WaitCallback委托指向的必須是一個帶有Object參數的無返回值方法,這無疑是一種限制。若方法需要有返回值,或者帶有多個參數,這將多費周折。有見及此,.NET提供了另一種方式去建立工作者線程,那就是委托。
4.4 委托類
使用CLR線程池中的工作者線程,最靈活最常用的方式就是使用委托的異步方法,在此先簡單介紹一下委托類。
當定義委托後,.NET就會自動創建一個代表該委托的類,下面可以用反射方式顯示委托類的方法成員(對反射有興趣的朋友可以先參考一下“.NET基礎篇——反射的奧妙”)
1 class Program 2 { 3 delegate void MyDelegate(); 4 5 static void Main(string[] args) 6 { 7 MyDelegate delegate1 = new MyDelegate(AsyncThread); 8 //顯示委托類的幾個方法成員 9 var methods=delegate1.GetType().GetMethods(); 10 if (methods != null) 11 foreach (MethodInfo info in methods) 12 Console.WriteLine(info.Name); 13 Console.ReadKey(); 14 } 15 }
委托類包括以下幾個重要方法
1 public class MyDelegate:MulticastDelegate 2 { 3 public MyDelegate(object target, int methodPtr); 4 //調用委托方法 5 public virtual void Invoke(); 6 //異步委托 7 public virtual IAsyncResult BeginInvoke(AsyncCallback callback,object state); 8 public virtual void EndInvoke(IAsyncResult result); 9 }
當調用Invoke()方法時,對應此委托的所有方法都會被執行。而BeginInvoke與EndInvoke則支持委托方法的異步調用,由BeginInvoke啟動的線程都屬於CLR線程池中的工作者線程,在下面將詳細說明。
4.5 利用BeginInvoke與EndInvoke完成異步委托方法
首先建立一個委托對象,通過IAsyncResult BeginInvoke(string name,AsyncCallback callback,object state) 異步調用委托方法,BeginInvoke 方法除最後的兩個參數外,其它參數都是與方法參數相對應的。通過 BeginInvoke 方法將返回一個實現了 System.IAsyncResult 接口的對象,之後就可以利用EndInvoke(IAsyncResult ) 方法就可以結束異步操作,獲取委托的運行結果。
1 class Program 2 { 3 delegate string MyDelegate(string name); 4 5 static void Main(string[] args) 6 { 7 ThreadMessage("Main Thread"); 8 9 //建立委托 10 MyDelegate myDelegate = new MyDelegate(Hello); 11 //異步調用委托,獲取計算結果 12 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); 13 //完成主線程其他工作 14 ............. 15 //等待異步方法完成,調用EndInvoke(IAsyncResult)獲取運行結果 16 string data=myDelegate.EndInvoke(result); 17 Console.WriteLine(data); 18 19 Console.ReadKey(); 20 } 21 22 static string Hello(string name) 23 { 24 ThreadMessage("Async Thread"); 25 Thread.Sleep(2000); //虛擬異步工作 26 return "Hello " + name; 27 } 28 29 //顯示當前線程 30 static void ThreadMessage(string data) 31 { 32 string message = string.Format("{0}\n ThreadId is:{1}", 33 data,Thread.CurrentThread.ManagedThreadId); 34 Console.WriteLine(message); 35 } 36 }
運行結果
4.6 善用IAsyncResult
在以上例子中可以看見,如果在使用myDelegate.BeginInvoke後立即調用myDelegate.EndInvoke,那在異步線程未完成工作以前主線程將處於阻塞狀態,等到異步線程結束獲取計算結果後,主線程才能繼續工作,這明顯無法展示出多線程的優勢。此時可以好好利用IAsyncResult 提高主線程的工作性能,IAsyncResult有以下成員:
1 public interface IAsyncResult 2 { 3 object AsyncState {get;} //獲取用戶定義的對象,它限定或包含關於異步操作的信息。 4 WailHandle AsyncWaitHandle {get;} //獲取用於等待異步操作完成的 WaitHandle。 5 bool CompletedSynchronously {get;} //獲取異步操作是否同步完成的指示。 6 bool IsCompleted {get;} //獲取異步操作是否已完成的指示。 7 }
通過輪詢方式,使用IsCompleted屬性判斷異步操作是否完成,這樣在異步操作未完成前就可以讓主線程執行另外的工作。
1 class Program 2 { 3 delegate string MyDelegate(string name); 4 5 static void Main(string[] args) 6 { 7 ThreadMessage("Main Thread"); 8 9 //建立委托 10 MyDelegate myDelegate = new MyDelegate(Hello); 11 //異步調用委托,獲取計算結果 12 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); 13 //在異步線程未完成前執行其他工作 14 while (!result.IsCompleted) 15 { 16 Thread.Sleep(200); //虛擬操作 17 Console.WriteLine("Main thead do work!"); 18 } 19 string data=myDelegate.EndInvoke(result); 20 Console.WriteLine(data); 21 22 Console.ReadKey(); 23 } 24 25 static string Hello(string name) 26 { 27 ThreadMessage("Async Thread"); 28 Thread.Sleep(2000); 29 return "Hello " + name; 30 } 31 32 static void ThreadMessage(string data) 33 { 34 string message = string.Format("{0}\n ThreadId is:{1}", 35 data,Thread.CurrentThread.ManagedThreadId); 36 Console.WriteLine(message); 37 } 38 }
運行結果:
除此以外,也可以使用WailHandle完成同樣的工作,WaitHandle裡面包含有一個方法WaitOne(int timeout),它可以判斷委托是否完成工作,在工作未完成前主線程可以繼續其他工作。運行下面代碼可得到與使用 IAsyncResult.IsCompleted 同樣的結果,而且更簡單方便 。
1 namespace Test 2 { 3 class Program 4 { 5 delegate string MyDelegate(string name); 6 7 static void Main(string[] args) 8 { 9 ThreadMessage("Main Thread"); 10 11 //建立委托 12 MyDelegate myDelegate = new MyDelegate(Hello); 13 14 //異步調用委托,獲取計算結果 15 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); 16 17 while (!result.AsyncWaitHandle.WaitOne(200)) 18 { 19 Console.WriteLine("Main thead do work!"); 20 } 21 string data=myDelegate.EndInvoke(result); 22 Console.WriteLine(data); 23 24 Console.ReadKey(); 25 } 26 27 static string Hello(string name) 28 { 29 ThreadMessage("Async Thread"); 30 Thread.Sleep(2000); 31 return "Hello " + name; 32 } 33 34 static void ThreadMessage(string data) 35 { 36 string message = string.Format("{0}\n ThreadId is:{1}", 37 data,Thread.CurrentThread.ManagedThreadId); 38 Console.WriteLine(message); 39 } 40 }
當要監視多個運行對象的時候,使用IAsyncResult.WaitHandle.WaitOne可就派不上用場了。 幸好.NET為WaitHandle准備了另外兩個靜態方法:WaitAny(waitHandle[], int)與WaitAll (waitHandle[] , int)。 其中WaitAll在等待所有waitHandle完成後再返回一個bool值。 而WaitAny是等待其中一個waitHandle完成後就返回一個int,這個int是代表已完成waitHandle在waitHandle[]中的數組索引。 下面就是使用WaitAll的例子,運行結果與使用 IAsyncResult.IsCompleted 相同。
1 class Program 2 { 3 delegate string MyDelegate(string name); 4 5 static void Main(string[] args) 6 { 7 ThreadMessage("Main Thread"); 8 9 //建立委托 10 MyDelegate myDelegate = new MyDelegate(Hello); 11 12 //異步調用委托,獲取計算結果 13 IAsyncResult result=myDelegate.BeginInvoke("Leslie", null, null); 14 15 //此處可加入多個檢測對象 16 WaitHandle[] waitHandleList = new WaitHandle[] { result.AsyncWaitHandle,........ }; 17 while (!WaitHandle.WaitAll(waitHandleList,200)) 18 { 19 Console.WriteLine("Main thead do work!"); 20 } 21 string data=myDelegate.EndInvoke(result); 22 Console.WriteLine(data); 23 24 Console.ReadKey(); 25 } 26 27 static string Hello(string name) 28 { 29 ThreadMessage("Async Thread"); 30 Thread.Sleep(2000); 31 return "Hello " + name; 32 } 33 34 static void ThreadMessage(string data) 35 { 36 string message = string.Format("{0}\n ThreadId is:{1}", 37 data,Thread.CurrentThread.ManagedThreadId); 38 Console.WriteLine(message); 39 } 40 }
4.7 回調函數
使用輪詢方式來檢測異步方法的狀態非常麻煩,而且效率不高,有見及此,.NET為 IAsyncResult BeginInvoke(AsyncCallback , object)准備了一個回調函數。使用 AsyncCallback 就可以綁定一個方法作為回調函數,回調函數必須是帶參數 IAsyncResult 且無返回值的方法: void AsycnCallbackMethod(IAsyncResult result) 。在BeginInvoke方法完成後,系統就會調用AsyncCallback所綁定的回調函數,最後回調函數中調用 XXX EndInvoke(IAsyncResult result) 就可以結束異步方法,它的返回值類型與委托的返回值一致。
1 class Program 2 { 3 delegate string MyDelegate(string name); 4 5 static void Main(string[] args) 6 { 7 ThreadMessage("Main Thread"); 8 9 //建立委托 10 MyDelegate myDelegate = new MyDelegate(Hello); 11 //異步調用委托,獲取計算結果 12 myDelegate.BeginInvoke("Leslie", new AsyncCallback(Completed), null); 13 //在啟動異步線程後,主線程可以繼續工作而不需要等待 14 for (int n = 0; n < 6; n++) 15 Console.WriteLine(" Main thread do work!"); 16 Console.WriteLine(""); 17 18 Console.ReadKey(); 19 } 20 21 static string Hello(string name) 22 { 23 ThreadMessage("Async Thread"); 24 Thread.Sleep(2000); \\模擬異步操作 25 return "\nHello " + name; 26 } 27 28 static void Completed(IAsyncResult result) 29 { 30 ThreadMessage("Async Completed"); 31 32 //獲取委托對象,調用EndInvoke方法獲取運行結果 33 AsyncResult _result = (AsyncResult)result; 34 MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate; 35 string data = myDelegate.EndInvoke(_result); 36 Console.WriteLine(data); 37 } 38 39 static void ThreadMessage(string data) 40 { 41 string message = string.Format("{0}\n ThreadId is:{1}", 42 data, Thread.CurrentThread.ManagedThreadId); 43 Console.WriteLine(message); 44 } 45 }
可以看到,主線在調用BeginInvoke方法可以繼續執行其他命令,而無需再等待了,這無疑比使用輪詢方式判斷異步方法是否完成更有優勢。 在異步方法執行完成後將會調用AsyncCallback所綁定的回調函數,注意一點,回調函數依然是在異步線程中執行,這樣就不會影響主線程的運行,這也使用回調函數最值得青昧的地方。 在回調函數中有一個既定的參數IAsyncResult,把IAsyncResult強制轉換為AsyncResult後,就可以通過 AsyncResult.AsyncDelegate 獲取原委托,再使用EndInvoke方法獲取計算結果。 運行結果如下:
如果想為回調函數傳送一些外部信息,就可以利用BeginInvoke(AsyncCallback,object)的最後一個參數object,它允許外部向回調函數輸入任何類型的參數。只需要在回調函數中利用 AsyncResult.AsyncState 就可以獲取object對象。
1 class Program 2 { 3 public class Person 4 { 5 public string Name; 6 public int Age; 7 } 8 9 delegate string MyDelegate(string name); 10 11 static void Main(string[] args) 12 { 13 ThreadMessage("Main Thread"); 14 15 //建立委托 16 MyDelegate myDelegate = new MyDelegate(Hello); 17 18 //建立Person對象 19 Person person = new Person(); 20 person.Name = "Elva"; 21 person.Age = 27; 22 23 //異步調用委托,輸入參數對象person, 獲取計算結果 24 myDelegate.BeginInvoke("Leslie", new AsyncCallback(Completed), person); 25 26 //在啟動異步線程後,主線程可以繼續工作而不需要等待 27 for (int n = 0; n < 6; n++) 28 Console.WriteLine(" Main thread do work!"); 29 Console.WriteLine(""); 30 31 Console.ReadKey(); 32 } 33 34 static string Hello(string name) 35 { 36 ThreadMessage("Async Thread"); 37 Thread.Sleep(2000); 38 return "\nHello " + name; 39 } 40 41 static void Completed(IAsyncResult result) 42 { 43 ThreadMessage("Async Completed"); 44 45 //獲取委托對象,調用EndInvoke方法獲取運行結果 46 AsyncResult _result = (AsyncResult)result; 47 MyDelegate myDelegate = (MyDelegate)_result.AsyncDelegate; 48 string data = myDelegate.EndInvoke(_result); 49 //獲取Person對象 50 Person person = (Person)result.AsyncState; 51 string message = person.Name + "'s age is " + person.Age.ToString(); 52 53 Console.WriteLine(data+"\n"+message); 54 } 55 56 static void ThreadMessage(string data) 57 { 58 string message = string.Format("{0}\n ThreadId is:{1}", 59 data, Thread.CurrentThread.ManagedThreadId); 60 Console.WriteLine(message); 61 } 62 }
運行結果:
關於I/O線程、SqlCommand多線程查詢、PLINQ、定時器與鎖的內容將在C#綜合揭秘——細說多線程(下)中詳細介紹。
a1 = 0x01; //0000 0001
a2 = 0x00; //0000 0000
a3 = 0x03; //0000 0011
a4 = 0x02; //0000 0010
b1 = a1 ^ a2; //0000 0001
b2 = a1 ^ a3; //0000 0010
b3 = a1 ^ a4; //0000 0011
^異或運算符,位值相同為0,不同為1,見上示例.
//
簡單實際問題舉例:
======\=======\=======
======a=======b=======
上面是2條電路,2個開關分別為a和b,打開狀態:\[1],關閉狀態:/[0].
若同時打開或者關閉,兩條電路均不通.
若a打開[1],b關閉[0],電路1通電
======\=======/=======
若a關閉[0],b打開[1],電路2通電
======/=======\=======
綜上,電路在a,b狀態相同時不通[0],在a,b不同時通電[1].
a1 = 0x01; //0000 0001
a2 = 0x00; //0000 0000
a3 = 0x03; //0000 0011
a4 = 0x02; //0000 0010
b1 = a1 ^ a2; //0000 0001
b2 = a1 ^ a3; //0000 0010
b3 = a1 ^ a4; //0000 0011
^異或運算符,位值相同為0,不同為1,見上示例.
//
簡單實際問題舉例:
======\=======\=======
======a=======b=======
上面是2條電路,2個開關分別為a和b,打開狀態:\[1],關閉狀態:/[0].
若同時打開或者關閉,兩條電路均不通.
若a打開[1],b關閉[0],電路1通電
======\=======/=======
若a關閉[0],b打開[1],電路2通電
======/=======\=======
綜上,電路在a,b狀態相同時不通[0],在a,b不同時通電[1].