程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C#綜合揭秘——細說多線程,

C#綜合揭秘——細說多線程,

編輯:C#入門知識

C#綜合揭秘——細說多線程,


一、線程的定義

 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     }
復制代碼

運行結果:

 

關於I/O線程、SqlCommand多線程查詢、PLINQ、定時器與鎖的內容將在C#綜合揭秘——細說多線程(下)中詳細介紹。


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].
 

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].
 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved