多執行緒的祕密

守望陽光01發表於2017-07-24

一、執行緒的定義

 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。
Interrupt() 中斷處於 WaitSleepJoin 執行緒狀態的執行緒。
Join() 已過載。 阻塞呼叫執行緒,直到某個執行緒終止時為止。
Resume() 繼續執行已掛起的執行緒。
Start()   執行本執行緒。
Suspend() 掛起當前執行緒,如果當前執行緒已屬於掛起狀態則此不起作用
Sleep()   把正在執行的執行緒掛起一段時間。

 

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是為非同步觸發帶引數的方法而設的,在下一節將為大家逐一細說。

委託說明
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()執行非同步方法。

複製程式碼
 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 }
複製程式碼

執行結果:

 

轉載於:http://www.cnblogs.com/leslies2/archive/2012/02/07/2310495.html  

相關文章