天天看點

C# 多線程

引言

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

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

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

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

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

目錄

一、線程的定義

二、線程的基礎知識

三、以ThreadStart方式實作多線程

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

五、CLR線程池的I/O線程

六、異步 SqlCommand

七、并行程式設計與PLINQ

八、計時器與鎖

 1. 1 程序、應用程式域與線程的關系

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

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

線程(Thread)是程序中的基本執行單元,在程序入口執行的第一個線程被視為這個程序的主線程。在.NET應用程式中,都是以Main()方法作為入口的,當調用此方法時系統就會自動建立一個主線程。線程主要是由CPU寄存器、調用棧和線程本地存儲器(Thread Local Storage,TLS)組成的。CPU寄存器主要記錄目前所執行線程的狀态,調用棧主要用于維護線程所調用到的記憶體與資料,TLS主要用于存放線程的狀态資訊。

程序、應用程式域、線程的關系如下圖,一個程序内可以包括多個應用程式域,也有包括多個線程,線程也可以穿梭于多個應用程式域當中。但在同一個時刻,線程隻會處于一個應用程式域内。

C# 多線程

由于本文是以介紹多線程技術為主題,對程序、應用程式域的介紹就到此為止。關于程序、線程、應用程式域的技術,在“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顯示目前線程資訊

C# 多線程
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         }      
C# 多線程

運作結果

C# 多線程

2.2  System.Threading 命名空間

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

類    
AutoResetEvent 通知正在等待的線程已發生事件。無法繼承此類。
管理目前線程的執行上下文。無法繼承此類。
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
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()執行異步方法。

C# 多線程
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       }      
C# 多線程

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

C# 多線程

3.2 使用ParameterizedThreadStart委托

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

C# 多線程
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     }      
C# 多線程

運作結果:

C# 多線程

3.3 前台線程與背景線程

注意以上兩個例子都沒有使用Console.ReadKey(),但系統依然會等待異步線程完成後才會結束。這是因為使用Thread.Start()啟動的線程預設為前台線程,而系統必須等待所有前台線程運作結束後,應用程式域才會自動解除安裝。

在第二節曾經介紹過線程Thread有一個屬性IsBackground,通過把此屬性設定為true,就可以把線程設定為背景線程!這時應用程式域将在主線程完成時就被解除安裝,而不會等待異步線程的運作。

3.4 挂起線程

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

C# 多線程
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     }      
C# 多線程

運作結果如下,此時應用程式域将在主線程運作5秒後自動結束

C# 多線程

但系統無法預知異步線程需要運作的時間,是以用通過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()可以保證應用程式域等待異步線程結束後才終止運作。

C# 多線程
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          }      
C# 多線程

運作結果如下

C# 多線程

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 。

C# 多線程
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     }      
C# 多線程
C# 多線程

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

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

C# 多線程
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     }      
C# 多線程
C# 多線程

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

4.4  委托類       

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

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

C# 多線程
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      }      
C# 多線程

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

C# 多線程
C# 多線程
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     }      
C# 多線程

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

4.5  利用BeginInvoke與EndInvoke完成異步委托方法

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

C# 多線程
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     }      
C# 多線程
C# 多線程

4.6  善用IAsyncResult

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

C# 多線程
1 public interface IAsyncResult
2 {
3     object AsyncState {get;}            //擷取使用者定義的對象,它限定或包含關于異步操作的資訊。
4     WailHandle AsyncWaitHandle {get;}   //擷取用于等待異步操作完成的 WaitHandle。
5     bool CompletedSynchronously {get;}  //擷取異步操作是否同步完成的訓示。
6     bool IsCompleted {get;}             //擷取異步操作是否已完成的訓示。
7 }      
C# 多線程

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

C# 多線程
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     }      
C# 多線程
C# 多線程

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

C# 多線程
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     }      
C# 多線程

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

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

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

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

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

C# 多線程
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     }      
C# 多線程

4.7 回調函數

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

C# 多線程
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     }      
C# 多線程

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

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

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

運作結果如下:

C# 多線程

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

C# 多線程
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     }      
C# 多線程

運作結果:

C# 多線程

關于I/O線程、SqlCommand多線程查詢、PLINQ、定時器與鎖的内容将繼續介紹

而 I/O線程可能容易遭到大家的忽略,其實在開發多線程系統,更應該多留意I/O線程的操作。特别是在ASP.NET開發當中,可能更多人隻會留意在用戶端使用Ajax或者在伺服器端使用UpdatePanel。其實合理使用I/O線程在通訊項目或檔案下載下傳時,能盡量降低IIS的壓力。

在前一節所介紹的線程都屬于CLR線程池的工作者線程,這一節開始為大家介紹一下CLR線程池的I/O線程

I/O 線程是.NET專為通路外部資源所設定的一種線程,因為通路外部資源常常要受到外界因素的影響,為了防止讓主線程受影響而長期處于阻塞狀态,.NET為多個I/O操作都建立起了異步方法,例如:FileStream、TCP/IP、WebRequest、WebService等等,而且每個異步方法的使用方式都非常類似,都是以BeginXXX為開始,以EndXXX結束,下面為大家一一解說。

5.1  異步讀寫 FileStream

需要在 FileStream 異步調用 I/O線程,必須使用以下構造函數建立 FileStream 對象,并把useAsync設定為 true。

FileStream stream = new FileStream ( string path, FileMode mode, FileAccess access, FileShare share, int bufferSize,bool useAsync ) ;

其中 path 是檔案的相對路徑或絕對路徑; mode 确定如何打開或建立檔案; access 确定通路檔案的方式; share 确定檔案如何程序共享; bufferSize 是代表緩沖區大小,一般預設最小值為8,在啟動異步讀取或寫入時,檔案大小一般大于緩沖大小; userAsync代表是否啟動異步I/O線程。

注意:當使用 BeginRead 和 BeginWrite 方法在執行大量讀或寫時效果更好,但對于少量的讀/寫,這些方法速度可能比同步讀取還要慢,因為進行線程間的切換需要大量時間。

5.1.1 異步寫入

FileStream中包含BeginWrite、EndWrite 方法可以啟動I/O線程進行異步寫入。

public override IAsyncResult BeginWrite ( byte[] array, int offset, int numBytes, AsyncCallback userCallback, Object stateObject )

public override void EndWrite (IAsyncResult asyncResult )

BeginWrite 傳回值為IAsyncResult, 使用方式與委托的BeginInvoke方法相似,最好就是使用回調函數,避免線程阻塞。在最後兩個參數中,參數AsyncCallback用于綁定回調函數; 參數Object用于傳遞外部資料。要注意一點:AsyncCallback所綁定的回調函數必須是帶單個 IAsyncResult 參數的無傳回值方法。

在例子中,把FileStream作為外部資料傳遞到回調函數當中,然後在回調函數中利用IAsyncResult.AsyncState擷取FileStream對象,最後通過FileStream.EndWrite(IAsyncResult)結束寫入。

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //把線程池的最大值設定為1000
 6             ThreadPool.SetMaxThreads(1000, 1000);
 7             ThreadPoolMessage("Start");
 8 
 9             //新立檔案File.sour
10             FileStream stream = new FileStream("File.sour", FileMode.OpenOrCreate, 
11                                        FileAccess.ReadWrite,FileShare.ReadWrite,1024,true);
12             byte[] bytes = new byte[16384];
13             string message = "An operating-system ThreadId has no fixed relationship........";
14             bytes = Encoding.Unicode.GetBytes(message);
15 
16             //啟動異步寫入
17             stream.BeginWrite(bytes, 0, (int)bytes.Length,new AsyncCallback(Callback),stream);
18             stream.Flush();
19             
20             Console.ReadKey();
21         }
22 
23         static void Callback(IAsyncResult result)
24         {
25             //顯示線程池現狀
26             Thread.Sleep(200);
27             ThreadPoolMessage("AsyncCallback");
28             //結束異步寫入
29             FileStream stream = (FileStream)result.AsyncState;
30             stream.EndWrite(result);
31             stream.Close();
32         }
33 
34         //顯示線程池現狀
35         static void ThreadPoolMessage(string data)
36         {
37             int a, b;
38             ThreadPool.GetAvailableThreads(out a, out b);
39             string message = string.Format("{0}\n  CurrentThreadId is {1}\n  "+
40                   "WorkerThreads is:{2}  CompletionPortThreads is :{3}",
41                   data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
42             Console.WriteLine(message);
43         }
44     }      
C# 多線程

由輸出結果可以看到,在使用FileStream.BeginWrite方法後,系統将自動啟動CLR線程池中I/O線程。

C# 多線程

5.1.2 異步讀取

FileStream 中包含 BeginRead 與 EndRead 可以異步調用I/O線程進行讀取。

public override IAsyncResult BeginRead ( byte[] array,int offset,int numBytes, AsyncCallback userCallback,Object stateObject)

public override int EndRead(IAsyncResult asyncResult)

其使用方式與BeginWrite和EndWrite相似,AsyncCallback用于綁定回調函數; Object用于傳遞外部資料。在回調函數隻需要使用IAsyncResut.AsyncState就可擷取外部資料。EndWrite 方法會傳回從流讀取到的位元組數量。

首先定義 FileData 類,裡面包含FileStream對象,byte[] 數組和長度。然後把FileData對象作為外部資料傳到回調函數,在回調函數中,把IAsyncResult.AsyncState強制轉換為FileData,然後通過FileStream.EndRead(IAsyncResult)結束讀取。最後比較一下長度,若讀取到的長度與輸入的資料長度不一至,則抛出異常。

C# 多線程
1      class Program
 2      {
 3          public class FileData
 4          {
 5              public FileStream Stream;
 6              public int Length;
 7              public byte[] ByteData;
 8          }
 9  
10          static void Main(string[] args)
11          {       
12              //把線程池的最大值設定為1000
13              ThreadPool.SetMaxThreads(1000, 1000);
14              ThreadPoolMessage("Start");
15              ReadFile();
16  
17              Console.ReadKey();
18          }
19  
20          static void ReadFile()
21          {
22              byte[] byteData=new byte[80961024];
23              FileStream stream = new FileStream("File1.sour", FileMode.OpenOrCreate, 
24                                      FileAccess.ReadWrite, FileShare.ReadWrite, 1024, true);
25              
26              //把FileStream對象,byte[]對象,長度等有關資料綁定到FileData對象中,以附帶屬性方式送到回調函數
27              FileData fileData = new FileData();
28              fileData.Stream = stream;
29              fileData.Length = (int)stream.Length;
30              fileData.ByteData = byteData;
31              
32              //啟動異步讀取
33              stream.BeginRead(byteData, 0, fileData.Length, new AsyncCallback(Completed), fileData);
34          }
35   
36          static void Completed(IAsyncResult result)
37          {
38              ThreadPoolMessage("Completed");
39  
40              //把AsyncResult.AsyncState轉換為FileData對象,以FileStream.EndRead完成異步讀取
41              FileData fileData = (FileData)result.AsyncState;
42              int length=fileData.Stream.EndRead(result);
43              fileData.Stream.Close();
44  
45              //如果讀取到的長度與輸入長度不一緻,則抛出異常
46              if (length != fileData.Length)
47                  throw new Exception("Stream is not complete!");
48  
49              string data=Encoding.ASCII.GetString(fileData.ByteData, 0, fileData.Length);
50              Console.WriteLine(data.Substring(2,22));
51          }
52  
53          //顯示線程池現狀
54          static void ThreadPoolMessage(string data)
55          {
56              int a, b;
57              ThreadPool.GetAvailableThreads(out a, out b);
58              string message = string.Format("{0}\n  CurrentThreadId is {1}\n  "+
59                           "WorkerThreads is:{2}  CompletionPortThreads is :{3}",
60                           data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
61              Console.WriteLine(message);      
62          }
63              
64    }      
C# 多線程

由輸出結果可以看到,在使用FileStream.BeginRead方法後,系統将自動啟動CLR線程池中I/O線程。

C# 多線程

注意:如果你看到的測試結果正好相反:工作者線程為999,I/O線程為1000,這是因為FileStream的檔案容量小于緩沖值1024所緻的。此時檔案将會一次性讀取或寫入,而系統将啟動工作者線程而非I/O線程來處理回調函數。

5.2 異步操作TCP/IP套接字

在介紹 TCP/IP 套接字前先簡單介紹一下 NetworkStream 類,它是用于網絡通路的基礎資料流。 NetworkStream 提供了好幾個方法控制套接字資料的發送與接收, 其中BeginRead、EndRead、BeginWrite、EndWrite 能夠實作異步操作,而且異步線程是來自于CLR線程池的I/O線程。

public override int ReadByte ()

public override int Read (byte[] buffer,int offset, int size)

public override void WriteByte (byte value)

public override void Write (byte[] buffer,int offset, int size)

public override IAsyncResult BeginRead (byte [] buffer, int offset, int size,  AsyncCallback callback, Object state )

public override int EndRead(IAsyncResult result)

public override IAsyncResult BeginWrite (byte [] buffer, int offset, int size,  AsyncCallback callback, Object state )

public override void EndWrite(IAsyncResult result)

若要建立 NetworkStream,必須提供已連接配接的 Socket。而在.NET中使用TCP/IP套接字不需要直接與Socket打交道,因為.NET把Socket的大部分操作都放在System.Net.TcpListener和System.Net.Sockets.TcpClient裡面,這兩個類大大地簡化了Socket的操作。一般套接字對象Socket包含一個Accept()方法,此方法能産生阻塞來等待用戶端的請求,而在TcpListener類裡也包含了一個相似的方法 public TcpClient AcceptTcpClient()用于等待用戶端的請求。此方法将會傳回一個TcpClient 對象,通過 TcpClient 的 public NetworkStream GetStream()方法就能擷取NetworkStream對象,控制套接字資料的發送與接收。

下面以一個例子說明異步調用TCP/IP套接字收發資料的過程。

首先在伺服器端建立預設位址127.0.0.1用于收發資訊,使用此位址與端口500建立TcpListener對象,調用TcpListener.Start 偵聽傳入的連接配接請求,再使用一個死循環來監聽資訊。

在ChatClient類包括有接收資訊與發送資訊兩個功能:當接收到用戶端請求時,它會利用 NetworkStream.BeginRead 讀取用戶端資訊,并在回調函數ReceiveAsyncCallback中輸出資訊内容,若接收到的資訊的大小小于1時,它将會抛出一個異常。當資訊成功接收後,再使用 NetworkStream.BeginWrite 方法回饋資訊到用戶端

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //設定CLR線程池最大線程數
 6             ThreadPool.SetMaxThreads(1000, 1000);
 7    
 8             //預設位址為127.0.0.1
 9             IPAddress ipAddress = IPAddress.Parse("127.0.0.1");
10             TcpListener tcpListener = new TcpListener(ipAddress, 500);
11             tcpListener.Start();
12             
13             //以一個死循環來實作監聽
14             while (true)
15             {   //調用一個ChatClient對象來實作監聽
16                 ChatClient chatClient = new ChatClient(tcpListener.AcceptTcpClient());    
17             }
18         }
19     }
20 
21     public class ChatClient
22     {
23         static TcpClient tcpClient;
24         static byte[] byteMessage;
25         static string clientEndPoint;
26 
27         public ChatClient(TcpClient tcpClient1)
28         {
29             tcpClient = tcpClient1;
30             byteMessage = new byte[tcpClient.ReceiveBufferSize];
31            
32             //顯示用戶端資訊
33             clientEndPoint = tcpClient.Client.RemoteEndPoint.ToString();
34             Console.WriteLine("Client's endpoint is " + clientEndPoint);
35             
36             //使用NetworkStream.BeginRead異步讀取資訊
37             NetworkStream networkStream = tcpClient.GetStream();
38             networkStream.BeginRead(byteMessage, 0, tcpClient.ReceiveBufferSize,
39                                          new AsyncCallback(ReceiveAsyncCallback), null);
40         }
41 
42         public void ReceiveAsyncCallback(IAsyncResult iAsyncResult)
43         {
44             //顯示CLR線程池狀态
45             Thread.Sleep(100);
46             ThreadPoolMessage("\nMessage is receiving");
47 
48             //使用NetworkStream.EndRead結束異步讀取
49             NetworkStream networkStreamRead = tcpClient.GetStream();
50             int length=networkStreamRead.EndRead(iAsyncResult);
51 
52             //如果接收到的資料長度少于1則抛出異常
53             if (length < 1)
54             {
55                 tcpClient.GetStream().Close();
56                 throw new Exception("Disconnection!");
57             }
58 
59             //顯示接收資訊
60             string message = Encoding.UTF8.GetString(byteMessage, 0, length);
61             Console.WriteLine("Message:" + message);
62 
63             //使用NetworkStream.BeginWrite異步發送資訊
64             byte[] sendMessage = Encoding.UTF8.GetBytes("Message is received!");
65             NetworkStream networkStreamWrite=tcpClient.GetStream();
66             networkStreamWrite.BeginWrite(sendMessage, 0, sendMessage.Length, 
67                                             new AsyncCallback(SendAsyncCallback), null);
68         }
69 
70         //把資訊轉換成二進制資料,然後發送到用戶端
71         public void SendAsyncCallback(IAsyncResult iAsyncResult)
72         {
73             //顯示CLR線程池狀态
74             Thread.Sleep(100);
75             ThreadPoolMessage("\nMessage is sending");
76 
77             //使用NetworkStream.EndWrite結束異步發送
78             tcpClient.GetStream().EndWrite(iAsyncResult);
79 
80             //重新監聽
81             tcpClient.GetStream().BeginRead(byteMessage, 0, tcpClient.ReceiveBufferSize,
82                                                new AsyncCallback(ReceiveAsyncCallback), null);
83         }
84 
85         //顯示線程池現狀
86         static void ThreadPoolMessage(string data)
87         {
88             int a, b;
89             ThreadPool.GetAvailableThreads(out a, out b);
90             string message = string.Format("{0}\n  CurrentThreadId is {1}\n  " +
91                   "WorkerThreads is:{2}  CompletionPortThreads is :{3}\n",
92                   data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
93 
94             Console.WriteLine(message);
95         }
96     }      
C# 多線程

而在用戶端隻是使用簡單的開發方式,利用TcpClient連接配接到伺服器端,然後調用NetworkStream.Write方法發送資訊,最後調用NetworkStream.Read方法讀取回饋資訊

C# 多線程
1         static void Main(string[] args)
 2         {
 3             //連接配接服務端
 4             TcpClient tcpClient = new TcpClient("127.0.0.1", 500);
 5 
 6             //發送資訊
 7             NetworkStream networkStream = tcpClient.GetStream();
 8             byte[] sendMessage = Encoding.UTF8.GetBytes("Client request connection!");
 9             networkStream.Write(sendMessage, 0, sendMessage.Length);
10             networkStream.Flush();
11 
12             //接收資訊
13             byte[] receiveMessage=new byte[1024];
14             int count=networkStream.Read(receiveMessage, 0,1024);
15             Console.WriteLine(Encoding.UTF8.GetString(receiveMessage));
16             Console.ReadKey();
17         }      
C# 多線程

注意觀察運作結果,伺服器端的異步操作線程都是來自于CLR線程池的I/O線程

C# 多線程

5.3 異步WebRequest

System.Net.WebRequest 是 .NET 為實作通路 Internet 的 “請求/響應模型” 而開發的一個 abstract 基類, 它主要有三個子類:FtpWebRequest、HttpWebRequest、FileWebRequest。當使用WebRequest.Create(string uri)建立對象時,應用程式就可以根據請求協定判斷實作類來進行操作。FileWebRequest、FtpWebRequest、HttpWebRequest 各有其作用:FileWebRequest 使用 “file://路徑” 的URI方式實作對本地資源和内部檔案的請求/響應、FtpWebRequest 使用FTP檔案傳輸協定實作檔案請求/響應、HttpWebRequest 用于處理HTTP的頁面請求/響應。由于使用方法相類似,下面就以常用的HttpWebRequest為例子介紹一下異步WebRequest的使用方法。

在使用ASP.NET開發網站的時候,往往會忽略了HttpWebRequest的使用,因為開發都假設用戶端是使用浏覽器等工具去閱讀頁面的。但如果你對REST開發方式有所了解,那對 HttpWebRequest 就應該非常熟悉。它可以在路徑參數、頭檔案、頁面主體、Cookie 等多處地方加入請求條件,然後對回複資料進行适當處理。HttpWebRequest 包含有以下幾個常用方法用于處理請求/響應:

public override Stream GetRequestStream ()

public override WebResponse GetResponse ()

public override IAsyncResult BeginGetRequestStream ( AsyncCallback callback, Object state )

public override Stream EndGetRequestStream ( IAsyncResult asyncResult )

public override IAsyncResult BeginGetResponse ( AsyncCallback callback, Object state )

public override WebResponse EndGetResponse ( IAsyncResult asyncResult )

其中BeginGetRequestStream、EndGetRequestStream 用于異步向HttpWebRequest對象寫入請求資訊;  BeginGetResponse、EndGetResponse 用于異步發送頁面請求并擷取傳回資訊。使用異步方式操作Internet的“請求/響應”,避免主線程長期處于等待狀态,而操作期間異步線程是來自CLR線程池的I/O線程。

注意:請求與響應不能使用同步與異步混合開發模式,即當請求寫入使用GetRequestStream同步模式,即使響應使用BeginGetResponse異步方法,操作也與GetRequestStream方法在于同一線程内。

下面以簡單的例子介紹一下異步請求的用法。

首先為Person類加上可序列化特性,在伺服器端建立Hanlder.ashx,通過Request.InputStream 擷取到請求資料并把資料轉化為String對象,此執行個體中資料是以 “Id:1” 的形式實作傳送的。然後根據Id查找對應的Person對象,并把Person對象寫入Response.OutStream 中返還到用戶端。

在用戶端先把 HttpWebRequird.Method 設定為 "post",使用異步方式通過BeginGetRequireStream擷取請求資料流,然後寫入請求資料 “Id:1”。再使用異步方法BeginGetResponse 擷取回複資料,最後把資料反序列化為Person對象顯示出來。

注意:HttpWebRequire.Method預設為get,在寫入請求前必須把HttpWebRequire.Method設定為post,否則在使用BeginGetRequireStream 擷取請求資料流的時候,系統就會發出 “無法發送具有此謂詞類型的内容正文" 的異常。

Model

C# 多線程
1 namespace Model
 2 {
 3     [Serializable]
 4     public class Person
 5     {
 6         public int ID
 7         {
 8             get;
 9             set;
10         }
11         public string Name
12         {
13             get;
14             set;
15         }
16         public int Age
17         {
18             get;
19             set;
20         }
21     }
22 }      
C# 多線程

伺服器端

C# 多線程
1 public class Handler : IHttpHandler {
 2 
 3     public void ProcessRequest(HttpContext context)
 4     {
 5         //把資訊轉換為String,找出輸入條件Id
 6         byte[] bytes=new byte[1024];
 7         int length=context.Request.InputStream.Read(bytes,0,1024);
 8         string condition = Encoding.Default.GetString(bytes);
 9         int id = int.Parse(condition.Split(new string[] { ":" }, 
10                            StringSplitOptions.RemoveEmptyEntries)[1]);
11         
12         //根據Id查找對應Person對象
13         var person = GetPersonList().Where(x => x.ID == id).First();
14         
15         //所Person格式化為二進制資料寫入OutputStream
16         BinaryFormatter formatter = new BinaryFormatter();
17         formatter.Serialize(context.Response.OutputStream, person);
18     }
19 
20     //模拟源資料
21     private IList<Person> GetPersonList()
22     {
23         var personList = new List<Person>();
24         
25         var person1 = new Person();
26         person1.ID = 1;
27         person1.Name = "Leslie";
28         person1.Age = 30;
29         personList.Add(person1);
30         ...........
31         return personList;
32     }
33 
34     public bool IsReusable
35     {
36         get { return true;}
37     }
38 }      
C# 多線程

用戶端

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             ThreadPool.SetMaxThreads(1000, 1000);
 6             Request();
 7             Console.ReadKey();
 8         }
 9 
10         static void Request()
11         {
12             ThreadPoolMessage("Start"); 
13             //使用WebRequest.Create方法建立HttpWebRequest對象
14             HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(
15                                             "http://localhost:5700/Handler.ashx");
16             webRequest.Method = "post";
17            
18             //對寫入資料的RequestStream對象進行異步請求
19             IAsyncResult result=webRequest.BeginGetRequestStream(
20                 new AsyncCallback(EndGetRequestStream),webRequest);
21         }
22 
23         static void EndGetRequestStream(IAsyncResult result)
24         {
25             ThreadPoolMessage("RequestStream Complete");
26             //擷取RequestStream
27             HttpWebRequest webRequest = (HttpWebRequest)result.AsyncState;
28             Stream stream=webRequest.EndGetRequestStream(result);
29 
30             //寫入請求條件
31             byte[] condition = Encoding.Default.GetBytes("Id:1");
32             stream.Write(condition, 0, condition.Length);
33 
34             //異步接收回傳資訊
35             IAsyncResult responseResult = webRequest.BeginGetResponse(
36                 new AsyncCallback(EndGetResponse), webRequest);
37         }
38 
39         static void EndGetResponse(IAsyncResult result)
40         {
41             //顯出線程池現狀
42             ThreadPoolMessage("GetResponse Complete");
43 
44             //結束異步請求,擷取結果
45             HttpWebRequest webRequest = (HttpWebRequest)result.AsyncState;
46             WebResponse webResponse = webRequest.EndGetResponse(result);
47             
48             //把輸出結果轉化為Person對象
49             Stream stream = webResponse.GetResponseStream();
50             BinaryFormatter formatter = new BinaryFormatter();
51             var person=(Person)formatter.Deserialize(stream);
52             Console.WriteLine(string.Format("Person    Id:{0} Name:{1} Age:{2}",
53                 person.ID, person.Name, person.Age));
54         }
55 
56         //顯示線程池現狀
57         static void ThreadPoolMessage(string data)
58         {
59             int a, b;
60             ThreadPool.GetAvailableThreads(out a, out b);
61             string message = string.Format("{0}\n  CurrentThreadId is {1}\n  " +
62                   "WorkerThreads is:{2}  CompletionPortThreads is :{3}\n",
63                   data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
64 
65             Console.WriteLine(message);
66         }
67     }      
C# 多線程

從運作結果可以看到,BeginGetRequireStream、BeginGetResponse方法是使用CLR線程池的I/O線程。

C# 多線程

5.4 異步調用WebService

相比TCP/IP套接字,在使用WebService的時候,伺服器端需要更複雜的操作處理,使用時間往往會更長。為了避免用戶端長期處于等待狀态,在配置服務引用時選擇 “生成異步操作”,系統可以自動建立異步調用的方式。

以.NET 2.0以前,系統都是使用ASMX來設計WebService,而近年來WCF可說是火熱登場,下面就以WCF為例子簡單介紹一下異步調用WebService的例子。

由于系統可以自動生成異步方法,使用起來非常簡單,首先在伺服器端建立服務ExampleService,裡面包含方法Method。用戶端引用此服務時,選擇 “生成異步操作”。然後使用 BeginMethod 啟動異步方法, 在回調函數中調用EndMethod結束異步調用。

服務端

C# 多線程
1      [ServiceContract]
 2      public interface IExampleService
 3      {
 4          [OperationContract]
 5          string Method(string name);
 6      }
 7  
 8      public class ExampleService : IExampleService
 9      {
10          public string Method(string name)
11          {
12              return "Hello " + name;
13          }
14      }
15  
16      class Program
17      {
18          static void Main(string[] args)
19          {
20              ServiceHost host = new ServiceHost(typeof(ExampleService));
21              host.Open();
22              Console.ReadKey();
23              host.Close();
24           }
25      }
26  
27  <configuration>
28      <system.serviceModel>
29          <services>
30              <service name="Example.ExampleService">
31                  <endpoint address="" binding="wsHttpBinding" contract="Example.IExampleService">
32                      <identity>
33                          <dns value="localhost" />
34                      </identity>
35                  </endpoint>
36                  <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
37                  <host>
38                      <baseAddresses>
39                          <add baseAddress="http://localhost:7200/Example/ExampleService/" />
40                      </baseAddresses>
41                  </host>
42              </service>
43          </services>
44      </system.serviceModel>
45  </configuration>      
C# 多線程
C# 多線程
1      class Program
 2      {
 3          static void Main(string[] args)
 4          {
 5              //設定最大線程數
 6              ThreadPool.SetMaxThreads(1000, 1000);
 7              ThreadPoolMessage("Start");
 8              
 9              //建立服務對象,異步調用服務方法
10              ExampleServiceReference.ExampleServiceClient exampleService = new
11                                      ExampleServiceReference.ExampleServiceClient();
12              exampleService.BeginMethod("Leslie",new AsyncCallback(AsyncCallbackMethod), 
13                                          exampleService);  
14              Console.ReadKey();
15          }
16  
17          static void AsyncCallbackMethod(IAsyncResult result)
18          {
19              Thread.Sleep(1000);
20              ThreadPoolMessage("Complete");
21              ExampleServiceReference.ExampleServiceClient example =
22                  (ExampleServiceReference.ExampleServiceClient)result.AsyncState;
23              string data=example.EndMethod(result);
24              Console.WriteLine(data);
25          }
26  
27          //顯示線程池現狀
28          static void ThreadPoolMessage(string data)
29          {
30              int a, b;
31              ThreadPool.GetAvailableThreads(out a, out b);
32              string message = string.Format("{0}\n  CurrentThreadId is {1}\n  " +
33                    "WorkerThreads is:{2}  CompletionPortThreads is :{3}\n",
34                    data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
35  
36              Console.WriteLine(message);
37          }
38      }
39  
40  <configuration>
41      <system.serviceModel>
42          <bindings>
43              <wsHttpBinding>
44                  <binding name="WSHttpBinding_IExampleService" closeTimeout="00:01:00"
45                      openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
46                      bypassProxyOnLocal="false" transactionFlow="false" 
47                      hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="524288"
48                      maxReceivedMessageSize="65536" messageEncoding="Text" textEncoding="utf-8"
49                      useDefaultWebProxy="true" allowCookies="false">
50                      <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
51                          maxBytesPerRead="4096" maxNameTableCharCount="16384" />
52                      <reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false" />
53                      <security mode="Message">
54                          <transport clientCredentialType="Windows" proxyCredentialType="None"
55                            realm="" />
56                          <message clientCredentialType="Windows" negotiateServiceCredential="true"
57                              algorithmSuite="Default" />
58                      </security>
59                  </binding>
60              </wsHttpBinding>
61          </bindings>
62          <client>
63              <endpoint address="http://localhost:7200/Example/ExampleService/"
64                  binding="wsHttpBinding" bindingConfiguration="WSHttpBinding_IExampleService"
65                  contract="ExampleServiceReference.IExampleService" 
66                  name="WSHttpBinding_IExampleService">
67                  <identity>
68                      <dns value="localhost" />
69                  </identity>
70              </endpoint>
71          </client>
72      </system.serviceModel>
73  </configuration>      
C# 多線程

注意觀察運作結果,異步調用服務時,回調函數都是運作于CLR線程池的I/O線程當中。

C# 多線程

回到目錄

從ADO.NET 2.0開始,SqlCommand就新增了幾個異步方法執行SQL指令。相對于同步執行方式,它使主線程不需要等待資料庫的傳回結果,在使用複雜性查詢或批量插入時将有效提高主線程的效率。使用異步SqlCommand的時候,請注意把ConnectionString 的 Asynchronous Processing 設定為 true 。

注意:SqlCommand異步操作的特别之處在于線程并不依賴于CLR線程池,而是由Windows内部提供,這比使用異步委托更有效率。但如果需要使用回調函數的時候,回調函數的線程依然是來自于CLR線程池的工作者線程。

SqlCommand有以下幾個方法支援異步操作:

public IAsyncResult BeginExecuteNonQuery (......)

public int EndExecuteNonQuery(IAsyncResult)

public IAsyncResult BeginExecuteReader(......)

public SqlDataReader EndExecuteReader(IAsyncResult)

public IAsyncResult BeginExecuteXmlReader (......)

public XmlReader EndExecuteXmlReader(IAsyncResult)

由于使用方式相似,此處就以 BeginExecuteNonQuery 為例子,介紹一下異步SqlCommand的使用。首先建立connectionString,注意把Asynchronous Processing設定為true來啟動異步指令,然後把SqlCommand.CommandText設定為 WAITFOR DELAY "0:0:3" 來虛拟資料庫操作。再通過BeginExecuteNonQuery啟動異步操作,利用輪詢方式監測操作情況。最後在操作完成後使用EndExecuteNonQuery完成異步操作。

C# 多線程
1     class Program
 2     {
 3         //把Asynchronous Processing設定為true
 4         static string connectionString = "Data Source=LESLIE-PC;Initial Catalog=Business;“+
 5                                          "Integrated Security=True;Asynchronous Processing=true";
 6 
 7         static void Main(string[] args)
 8         {
 9             //把CLR線程池最大線程數設定為1000
10             ThreadPool.SetMaxThreads(1000, 1000);
11             ThreadPoolMessage("Start");
12 
13             //使用WAITFOR DELAY指令來虛拟操作
14             SqlConnection connection = new SqlConnection(connectionString);
15             SqlCommand command = new SqlCommand("WAITFOR DELAY '0:0:3';", connection);
16             connection.Open();
17 
18             //啟動異步SqlCommand操作,利用輪詢方式監測操作
19             IAsyncResult result = command.BeginExecuteNonQuery();
20             ThreadPoolMessage("BeginRead");
21             while (!result.AsyncWaitHandle.WaitOne(500))
22                 Console.WriteLine("Main thread do work........");
23 
24             //結束異步SqlCommand
25             int count= command.EndExecuteNonQuery(result);
26             ThreadPoolMessage("\nCompleted");
27             Console.ReadKey();
28         }
29 
30         //顯示線程池現狀
31         static void ThreadPoolMessage(string data)
32         {
33             int a, b;
34             ThreadPool.GetAvailableThreads(out a, out b);
35             string message = string.Format("{0}\n  CurrentThreadId is {1}\n  "+
36                    "WorkerThreads is:{2}  CompletionPortThreads is :{3}\n",
37                    data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
38             Console.WriteLine(message);
39         }
40     }      
C# 多線程

注意運作結果,SqlCommand的異步執行線程并不屬于CLR線程池。

C# 多線程

如果覺得使用輪詢方式過于麻煩,可以使用回調函數,但要注意當調用回調函數時,線程是來自于CLR線程池的工作者線程。

C# 多線程
1     class Program
 2     {
 3         //把Asynchronous Processing設定為true
 4         static string connectionString = "Data Source=LESLIE-PC;Initial Catalog=Business;”+
 5                                          “Integrated Security=True;Asynchronous Processing=true";
 6         static void Main(string[] args)
 7         {
 8             //把CLR線程池最大線程數設定為1000
 9             ThreadPool.SetMaxThreads(1000, 1000);
10             ThreadPoolMessage("Start");
11 
12             //使用WAITFOR DELAY指令來虛拟操作
13             SqlConnection connection = new SqlConnection(connectionString);
14             SqlCommand command = new SqlCommand("WAITFOR DELAY '0:0:3';", connection);
15             connection.Open();
16             
17             //啟動異步SqlCommand操作,并把SqlCommand對象傳遞到回調函數
18             IAsyncResult result = command.BeginExecuteNonQuery(
19                                        new AsyncCallback(AsyncCallbackMethod),command);
20             Console.ReadKey();
21         }
22 
23         static void AsyncCallbackMethod(IAsyncResult result)
24         {
25             Thread.Sleep(200);
26             ThreadPoolMessage("AsyncCallback");
27             SqlCommand command = (SqlCommand)result.AsyncState;
28             int count=command.EndExecuteNonQuery(result);
29             command.Connection.Close();
30         }
31 
32         //顯示線程池現狀
33         static void ThreadPoolMessage(string data)
34         {
35             int a, b;
36             ThreadPool.GetAvailableThreads(out a, out b);
37             string message = string.Format("{0}\n  CurrentThreadId is {1}\n  "+
38                   "WorkerThreads is:{2}  CompletionPortThreads is :{3}\n",
39                   data, Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
40 
41             Console.WriteLine(message);
42         }
43     }      
C# 多線程
C# 多線程

要使用多線程開發,必須非常熟悉Thread的使用,而且在開發過程中可能會面對很多未知的問題。為了簡化開發,.NET 4.0 特别提供一個并行程式設計庫System.Threading.Tasks,它可以簡化并行開發,你無需直接跟線程或線程池打交道,就可以簡單建立多線程應用程式。此外,.NET還提供了新的一組擴充方法PLINQ,它具有自動分析查詢功能,如果并行查詢能提高系統效率,則同時運作,如果查詢未能從并行查詢中受益,則按原順序查詢。下面将詳細介紹并行操作的方式。

7.1 泛型委托

使用并行程式設計可以同時操作多個委托,在介紹并行程式設計前先簡單介紹一下兩個泛型委托System.Func<>與System.Action<>。

Func<>是一個能接受多個參數和一個傳回值的泛型委托,它能接受0個到16個輸入參數, 其中 T1,T2,T3,T4......T16 代表自定的輸入類型,TResult為自定義的傳回值。

public delegate TResult Func<TResult>()

public delegate TResult Func<T1,TResult>(T1 arg1)

public delegate TResult Func<T1,T2, TResult>(T1 arg1,T2 arg2)

public delegate TResult Func<T1,T2, T3, TResult>(T1 arg1,T2 arg2,T3 arg3)

public delegate TResult Func<T1,T2, T3, ,T4, TResult>(T1 arg1,T2 arg2,T3 arg3,T4 arg4)

..............

public delegate TResult Func<T1,T2, T3, ,T4, ...... ,T16,TResult>(T1 arg1,T2 arg2,T3 arg3,T4 arg4,...... ,T16 arg16)

Action<>與Func<>十分相似,不同在于Action<>的傳回值為void,Action能接受0~16個參數

public delegate void Action<T1>()

public delegate void Action<T1,T2>(T1 arg1,T2 arg2)

public delegate void Action<T1,T2, T3>(T1 arg1,T2 arg2, T3 arg3)

.............

public delegate void Action<T1,T2, T3, ,T4, ...... ,T16>(T1 arg1,T2 arg2,T3 arg3,T4 arg4,...... ,T16 arg16)

7.2 任務并行庫(TPL)

System.Threading.Tasks中的類被統稱為任務并行庫(Task Parallel Library,TPL),TPL使用CLR線程池把工作配置設定到CPU,并能自動處理工作分區、線程排程、取消支援、狀态管理以及其他低級别的細節操作,極大地簡化了多線程的開發。

注意:TPL比Thread更具智能性,當它判斷任務集并沒有從并行運作中受益,就會選擇按順序運作。但并非所有的項目都适合使用并行開發,建立過多并行任務可能會損害程式的性能,降低運作效率。

TPL包括常用的資料并行與任務并行兩種執行方式:

7.2.1 資料并行

資料并行的核心類就是System.Threading.Tasks.Parallel,它包含兩個靜态方法 Parallel.For 與 Parallel.ForEach, 使用方式與for、foreach相仿。通過這兩個方法可以并行處理System.Func<>、System.Action<>委托。

以下一個例子就是利用 public static ParallelLoopResult For( int from, int max, Action<int>) 方法對List<Person>進行并行查詢。

假設使用單線程方式查詢3個Person對象,需要用時大約6秒,在使用并行方式,隻需使用2秒就能完成查詢,而且能夠避開Thread的繁瑣處理。

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //設定最大線程數
 6             ThreadPool.SetMaxThreads(1000, 1000);
 7             //并行查詢
 8             Parallel.For(0, 3,n =>
 9                 {
10                     Thread.Sleep(2000);  //模拟查詢
11                     ThreadPoolMessage(GetPersonList()[n]);
12                 });
13             Console.ReadKey();
14         }
15 
16         //模拟源資料
17         static IList<Person> GetPersonList()
18         {
19             var personList = new List<Person>();
20 
21             var person1 = new Person();
22             person1.ID = 1;
23             person1.Name = "Leslie";
24             person1.Age = 30;
25             personList.Add(person1);
26             ...........
27             return personList;
28         }
29 
30         //顯示線程池現狀
31         static void ThreadPoolMessage(Person person)
32         {
33             int a, b;
34             ThreadPool.GetAvailableThreads(out a, out b);
35             string message = string.Format("Person  ID:{0} Name:{1} Age:{2}\n" +
36                   "  CurrentThreadId is {3}\n  WorkerThreads is:{4}" +
37                   "  CompletionPortThreads is :{5}\n",
38                   person.ID, person.Name, person.Age,
39                   Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
40 
41             Console.WriteLine(message);
42         }
43     }      
C# 多線程

觀察運作結果,對象并非按照原排列順序進行查詢,而是使用并行方式查詢。

C# 多線程

若想停止操作,可以利用ParallelLoopState參數,下面以ForEach作為例子。

public static ParallelLoopResult ForEach<TSource>( IEnumerable<TSource> source, Action<TSource, ParallelLoopState> action)

其中source為資料集,在Action<TSource,ParallelLoopState>委托的ParallelLoopState參數當中包含有Break()和 Stop()兩個方法都可以使疊代停止。Break的使用跟傳統for裡面的使用方式相似,但因為處于并行處理當中,使用Break并不能保證所有運作能立即停止,在目前疊代之前的疊代會繼續執行。若想立即停止操作,可以使用Stop方法,它能保證立即終止所有的操作,無論它們是處于目前疊代的之前還是之後。

C# 多線程
1     class Program
 2     {
 3          static void Main(string[] args)
 4          {
 5              //設定最大線程數
 6              ThreadPool.SetMaxThreads(1000, 1000);
 7  
 8              //并行查詢
 9              Parallel.ForEach(GetPersonList(), (person, state) =>
10                  {
11                      if (person.ID == 2)
12                          state.Stop();
13                      ThreadPoolMessage(person);
14                  });
15              Console.ReadKey();
16          }
17  
18          //模拟源資料
19          static IList<Person> GetPersonList()
20          {
21              var personList = new List<Person>();
22  
23              var person1 = new Person();
24              person1.ID = 1;
25              person1.Name = "Leslie";
26              person1.Age = 30;
27              personList.Add(person1);
28              ..........
29              return personList;
30          }
31  
32          //顯示線程池現狀
33          static void ThreadPoolMessage(Person person)
34          {
35              int a, b;
36              ThreadPool.GetAvailableThreads(out a, out b);
37              string message = string.Format("Person  ID:{0} Name:{1} Age:{2}\n" +
38                    "  CurrentThreadId is {3}\n  WorkerThreads is:{4}" +
39                    "  CompletionPortThreads is :{5}\n",
40                    person.ID, person.Name, person.Age,
41                    Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
42  
43              Console.WriteLine(message);
44          }
45      }      
C# 多線程

觀察運作結果,當Person的ID等于2時,運作将會停止。

C# 多線程

當要在多個線程中調用本地變量,可以使用以下方法:

public static ParallelLoopResult ForEach<TSource, TLocal>(IEnumerable<Of TSource>, Func<Of TLocal>, Func<Of TSource,ParallelLoopState,TLocal,TLocal>, Action<Of TLocal>)

其中第一個參數為資料集;

第二個參數是一個Func委托,用于在每個線程執行前進行初始化;

第 三個參數是委托Func<Of T1,T2,T3,TResult>,它能對資料集的每個成員進行疊代,當中T1是資料集的成員,T2是一個ParallelLoopState對 象,它可以控制疊代的狀态,T3是線程中的本地變量;

第四個參數是一個Action委托,用于對每個線程的最終狀态進行最終操作。

在以下例子中,使用ForEach計算多個Order的總體價格。在ForEach方法中,首先把參數初始化為0f,然後用把同一個Order的多個OrderItem價格進行累加,計算出Order的價格,最後把多個Order的價格進行累加,計算出多個Order的總體價格。

C# 多線程
1     public class Order
 2     {
 3         public int ID;
 4         public float Price;
 5     }
 6 
 7     public class OrderItem
 8     {
 9         public int ID;
10         public string Goods;
11         public int OrderID;
12         public float Price;
13         public int Count;
14     }
15 
16     class Program
17     {
18         static void Main(string[] args)
19         {
20             //設定最大線程數
21             ThreadPool.SetMaxThreads(1000, 1000);
22             float totalPrice = 0f;
23             //并行查詢
24             var parallelResult = Parallel.ForEach(GetOrderList(),
25                      () => 0f,   //把參數初始值設為0
26                      (order, state, orderPrice) =>
27                      {
28                          //計算單個Order的價格
29                          orderPrice = GetOrderItem().Where(item => item.OrderID == order.ID)
30                               .Sum(item => item.Price * item.Count);
31                          order.Price = orderPrice;
32                          ThreadPoolMessage(order);
33                          
34                          return orderPrice;
35                      },
36                     (finallyPrice) =>
37                     {
38                         totalPrice += finallyPrice;//計算多個Order的總體價格
39                     }
40                 );
41             
42             while (!parallelResult.IsCompleted)
43                 Console.WriteLine("Doing Work!");
44 
45             Console.WriteLine("Total Price is:" + totalPrice);
46             Console.ReadKey();
47         }
48         //虛拟資料
49         static IList<Order> GetOrderList()
50         {
51             IList<Order> orderList = new List<Order>();
52             Order order1 = new Order();
53             order1.ID = 1;
54             orderList.Add(order1);
55             ............
56             return orderList;
57         }
58         //虛拟資料
59         static IList<OrderItem> GetOrderItem()
60         {
61             IList<OrderItem> itemList = new List<OrderItem>();
62 
63             OrderItem orderItem1 = new OrderItem();
64             orderItem1.ID = 1;
65             orderItem1.Goods = "iPhone 4S";
66             orderItem1.Price = 6700;
67             orderItem1.Count = 2;
68             orderItem1.OrderID = 1;
69             itemList.Add(orderItem1);
70             ...........
71             return itemList;
72         }
73 
74         //顯示線程池現狀
75         static void ThreadPoolMessage(Order order)
76         {
77             int a, b;
78             ThreadPool.GetAvailableThreads(out a, out b);
79             string message = string.Format("OrderID:{0}  OrderPrice:{1}\n" +
80                   "  CurrentThreadId is {2}\n  WorkerThreads is:{3}" +
81                   "  CompletionPortThreads is:{4}\n",
82                   order.ID, order.Price,
83                   Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
84 
85             Console.WriteLine(message);
86         }
87     }      
C# 多線程
C# 多線程

 7.2.2 任務并行

在TPL當中還可以使用Parallel.Invoke方法觸發多個異步任務,其中 actions 中可以包含多個方法或者委托,parallelOptions用于配置Parallel類的操作。

public static void Invoke(Action[] actions )

public static void Invoke(ParallelOptions parallelOptions, Action[] actions )

下面例子中利用了Parallet.Invoke并行查詢多個Person,actions當中可以綁定方法、lambda表達式或者委托,注意綁定方法時必須是傳回值為void的無參數方法。

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             //設定最大線程數
 6             ThreadPool.SetMaxThreads(1000, 1000);
 7             
 8             //任務并行
 9             Parallel.Invoke(option,
10                 PersonMessage, 
11                 ()=>ThreadPoolMessage(GetPersonList()[1]),  
12                 delegate(){
13                     ThreadPoolMessage(GetPersonList()[2]);
14                 });
15             Console.ReadKey();
16         }
17 
18         static void PersonMessage()
19         {
20             ThreadPoolMessage(GetPersonList()[0]);
21         }
22 
23         //顯示線程池現狀
24         static void ThreadPoolMessage(Person person)
25         {
26             int a, b;
27             ThreadPool.GetAvailableThreads(out a, out b);
28             string message = string.Format("Person  ID:{0} Name:{1} Age:{2}\n" +
29                   "  CurrentThreadId is {3}\n  WorkerThreads is:{4}" +
30                   "  CompletionPortThreads is :{5}\n",
31                   person.ID, person.Name, person.Age,
32                   Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
33 
34             Console.WriteLine(message);
35         }
36 
37         //模拟源資料
38         static IList<Person> GetPersonList()
39         {
40             var personList = new List<Person>();
41 
42             var person1 = new Person();
43             person1.ID = 1;
44             person1.Name = "Leslie";
45             person1.Age = 30;
46             personList.Add(person1);
47             ..........
48             return personList;
49         }
50     }      
C# 多線程
C# 多線程

7.3 Task簡介

以Thread建立的線程被預設為前台線程,當然你可以把線程IsBackground屬性設定為true,但TPL為此提供了一個更簡單的類Task。

Task存在于System.Threading.Tasks命名空間當中,它可以作為異步委托的簡單替代品。

通過Task的Factory屬性将傳回TaskFactory類,以TaskFactory.StartNew(Action)方法可以建立一個新線程,所建立的線程預設為背景線程。

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             ThreadPool.SetMaxThreads(1000, 1000);
 6             Task.Factory.StartNew(() => ThreadPoolMessage());
 7             Console.ReadKey();
 8         }
 9 
10         //顯示線程池現狀
11         static void ThreadPoolMessage()
12         {
13             int a, b;
14             ThreadPool.GetAvailableThreads(out a, out b);
15             string message = string.Format("CurrentThreadId is:{0}\n" +
16                 "CurrentThread IsBackground:{1}\n" +
17                 "WorkerThreads is:{2}\nCompletionPortThreads is:{3}\n",
18                  Thread.CurrentThread.ManagedThreadId,
19                  Thread.CurrentThread.IsBackground.ToString(),
20                  a.ToString(), b.ToString());
21             Console.WriteLine(message);
22         }
23     }      
C# 多線程
C# 多線程

若要取消處理,可以利用CancellationTakenSource對象,在TaskFactory中包含有方法

public Task StartNew( Action action, CancellationToken cancellationToken )

在方法中加入CancellationTakenSource對象的CancellationToken屬性,可以控制任務的運作,調用CancellationTakenSource.Cancel時任務就會自動停止。下面以圖檔下載下傳為例子介紹一下TaskFactory的使用。

伺服器端頁面

C# 多線程
1 <html xmlns="http://www.w3.org/1999/xhtml">
 2 <head runat="server">
 3     <title></title>
 4     <script type="text/C#" runat="server">
 5         private static List<string> url=new List<string>();
 6 
 7         protected void Page_Load(object sender, EventArgs e)
 8         {
 9             if (!Page.IsPostBack)
10             {
11                 url.Clear();
12                 Application["Url"] = null;
13             }
14         }
15 
16         protected void CheckBox_CheckedChanged(object sender, EventArgs e)
17         {
18             CheckBox checkBox = (CheckBox)sender;
19             if (checkBox.Checked)
20                 url.Add(checkBox.Text);
21             else
22                 url.Remove(checkBox.Text);
23             Application["Url"]= url;
24         }
25 </script>
26 </head>
27 <body>
28     <form id="form1" runat="server" >
29     <div align="left">
30        <div align="center" style="float: left;">
31          <asp:Image ID="Image1" runat="server" ImageUrl="~/Images/A.jpg" /><br />
32          <asp:CheckBox ID="CheckBox1" runat="server" AutoPostBack="True" 
33                oncheckedchanged="CheckBox_CheckedChanged" Text="A.jpg" />
34        </div>
35        <div align="center" style="float: left">
36           <asp:Image ID="Image2" runat="server" ImageUrl="~/Images/B.jpg" /><br />
37           <asp:CheckBox ID="CheckBox2" runat="server" AutoPostBack="True" 
38                oncheckedchanged="CheckBox_CheckedChanged" Text="B.jpg" />
39        </div>
40        <div align="center" style="float: left">
41           <asp:Image ID="Image3" runat="server" ImageUrl="~/Images/C.jpg" /><br />
42           <asp:CheckBox ID="CheckBox3" runat="server" AutoPostBack="True" 
43                oncheckedchanged="CheckBox_CheckedChanged" Text="C.jpg" />
44        </div>
45        <div align="center" style="float: left">
46           <asp:Image ID="Image4" runat="server" ImageUrl="~/Images/D.jpg" /><br />
47           <asp:CheckBox ID="CheckBox4" runat="server" AutoPostBack="True" 
48                oncheckedchanged="CheckBox_CheckedChanged" Text="D.jpg" />
49        </div>
50        <div align="center" style="float: left">
51           <asp:Image ID="Image5" runat="server" ImageUrl="~/Images/E.jpg" /><br />
52           <asp:CheckBox ID="CheckBox5" runat="server" AutoPostBack="True" 
53                oncheckedchanged="CheckBox_CheckedChanged" Text="E.jpg" />
54        </div>        
55     </div>
56     </form>
57 </body>
58 </html>      
C# 多線程

首先在伺服器頁面中顯示多個*.jpg圖檔,每個圖檔都有對應的CheckBox檢測其選擇情況。

所選擇圖檔的路徑會記錄在Application["Url"]當中傳遞到Handler.ashx當中。

注意:Application是一個全局變量,此處隻是為了顯示Task的使用方式,在ASP.NET開發應該慎用Application。

Handler.ashx 處理圖檔的下載下傳,它從 Application["Url"] 當中擷取所選擇圖檔的路徑,并把圖檔轉化成byte[]二進制資料。

再把圖檔的數量,每副圖檔的二進制資料的長度記錄在OutputStream的頭部。

最後把圖檔的二進制資料記入 OutputStream 一并輸出。

C# 多線程
1 public class Handler : IHttpHandler 
 2 {
 3     public void ProcessRequest(HttpContext context)
 4     {
 5         //擷取圖檔名,把圖檔數量寫OutputStream
 6         List<String> urlList = (List<string>)context.Application["Url"];
 7         context.Response.OutputStream.Write(BitConverter.GetBytes(urlList.Count), 0, 4);
 8         
 9         //把圖檔轉換成二進制資料
10         List<string> imageList = GetImages(urlList);
11         
12         //把每副圖檔長度寫入OutputStream
13         foreach (string image in imageList)
14         {
15             byte[] imageByte=Convert.FromBase64String(image);
16             context.Response.OutputStream.Write(BitConverter.GetBytes(imageByte.Length),0,4);
17         }
18         
19         //把圖檔寫入OutputStream
20         foreach (string image in imageList)
21         {
22             byte[] imageByte = Convert.FromBase64String(image);
23             context.Response.OutputStream.Write(imageByte,0,imageByte.Length);
24         }
25     }
26 
27     //擷取多個圖檔的二進制資料
28     private List<string> GetImages(List<string> urlList)
29     {
30         List<string> imageList = new List<string>();
31         foreach (string url in urlList)
32             imageList.Add(GetImage(url));
33         return imageList;
34     }
35     
36     //擷取單副圖檔的二進制資料
37     private string GetImage(string url)
38     {
39         string path = "E:/My Projects/Example/WebSite/Images/"+url;
40         FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read);
41         byte[] imgBytes = new byte[10240];
42         int imgLength = stream.Read(imgBytes, 0, 10240);       
43         return Convert.ToBase64String(imgBytes,0,imgLength);
44     }
45 
46     public bool IsReusable
47     {
48         get{ return false;}
49     }
50 }      
C# 多線程

建立一個WinForm視窗,裡面加入一個WebBrowser連接配接到伺服器端的Default.aspx頁面。

當按下Download按鍵時,系統就會利用TaskFactory.StartNew的方法建立異步線程,使用WebRequest方法向Handler.ashx發送請求。

接收到回傳流時,就會根據頭檔案的内容判斷圖檔的數量與每副圖檔的長度,把二進制資料轉化為*.jpg檔案儲存。

C# 多線程

系統利用TaskFactory.StartNew(action,cancellationToken) 方式異步調用GetImages方法進行圖檔下載下傳。 

當使用者按下Cancel按鈕時,異步任務就會停止。值得注意的是,在圖檔下載下傳時調用了CancellationToken.ThrowIfCancellationRequested方法,目的在檢查并行任務的運作情況,在并行任務被停止時釋放出OperationCanceledException異常,確定使用者按下Cancel按鈕時,停止所有并行任務。

C# 多線程
1     public partial class Form1 : Form
 2     {
 3         private CancellationTokenSource tokenSource = new CancellationTokenSource();
 4         
 5         public Form1()
 6         {
 7             InitializeComponent();
 8             ThreadPool.SetMaxThreads(1000, 1000);
 9         }
10 
11         private void downloadToolStripMenuItem_Click(object sender, EventArgs e)
12         {
13              Task.Factory.StartNew(GetImages,tokenSource.Token);
14         }
15 
16         private void cancelToolStripMenuItem_Click(object sender, EventArgs e)
17         {
18             tokenSource.Cancel();
19         }
20 
21         private void GetImages()
22         {
23             //發送請求,擷取輸出流
24             WebRequest webRequest = HttpWebRequest.Create("Http://localhost:5800/Handler.ashx"); 
25             Stream responseStream=webRequest.GetResponse().GetResponseStream();
26 
27             byte[] responseByte = new byte[81960];
28             IAsyncResult result=responseStream.BeginRead(responseByte,0,81960,null,null);
29             int responseLength = responseStream.EndRead(result);
30 
31             //擷取圖檔數量
32             int imageCount = BitConverter.ToInt32(responseByte, 0);
33             
34             //擷取每副圖檔的長度
35             int[] lengths = new int[imageCount];
36             for (int n = 0; n < imageCount; n++)
37             {
38                 int length = BitConverter.ToInt32(responseByte, (n + 1) * 4);
39                 lengths[n] = length;
40             }
41             try
42             {
43                 //儲存圖檔
44                 for (int n = 0; n < imageCount; n++)
45                 {
46                     string path = string.Format("E:/My Projects/Example/Test/Images/pic{0}.jpg", n);
47                     FileStream file = new FileStream(path, FileMode.Create, FileAccess.ReadWrite);
48 
49                     //計算位元組偏移量
50                     int offset = (imageCount + 1) * 4;
51                     for (int a = 0; a < n; a++)
52                         offset += lengths[a];
53 
54                     file.Write(responseByte, offset, lengths[n]);
55                     file.Flush();
56 
57                     //模拟操作
58                     Thread.Sleep(1000);
59 
60                     //檢測CancellationToken變化
61                     tokenSource.Token.ThrowIfCancellationRequested();
62                 }
63             }
64             catch (OperationCanceledException ex)
65             {
66                 MessageBox.Show("Download cancel!");
67             }
68         }
69     }      
C# 多線程

7.4 并行查詢(PLINQ)

并行 LINQ (PLINQ) 是 LINQ 模式的并行實作,主要差別在于 PLINQ 嘗試充分利用系統中的所有處理器。 它利用所有處理器的方法,把資料源分成片段,然後在多個處理器上對單獨工作線程上的每個片段并行執行查詢, 在許多情況下,并行執行意味着查詢運作速度顯著提高。但這并不說明所有PLINQ都會使用并行方式,當系統測試要并行查詢會對系統性能造成損害時,那将自動化地使用同步執行。

在System.Linq.ParallelEnumerable類中,包含了并行查詢的大部分方法。

方法成員 
AsParallel PLINQ 的入口點。 指定如果可能,應并行化查詢的其餘部分。
AsSequential(Of TSource) 指定查詢的其餘部分應像非并行 LINQ 查詢一樣按順序運作。
AsOrdered 指定 PLINQ 應保留查詢的其餘部分的源序列排序,直到例如通過使用 orderby(在 Visual Basic 中為 Order By)子句更改排序為止。
AsUnordered(Of TSource) 指定查詢的其餘部分的 PLINQ 不需要保留源序列的排序。
WithCancellation(Of TSource) 指定 PLINQ 應定期監視請求取消時提供的取消标記和取消執行的狀态。
WithDegreeOfParallelism(Of TSource) 指定 PLINQ 應當用來并行化查詢的處理器的最大數目。
WithMergeOptions(Of TSource) 提供有關 PLINQ 應當如何(如果可能)将并行結果合并回到使用線程上的一個序列的提示。
WithExecutionMode(Of TSource) 指定 PLINQ 應當如何并行化查詢(即使預設行為是按順序運作查詢)。
ForAll(Of TSource) 多線程枚舉方法,與循環通路查詢結果不同,它允許在不首先合并回到使用者線程的情況下并行處理結果。
Aggregate 重載 對于 PLINQ 唯一的重載,它啟用對線程本地分區的中間聚合以及一個用于合并所有分區結果的最終聚合函數。

7.4.1 AsParallel

通常想要實作并行查詢,隻需向資料源添加 AsParallel 查詢操作即可。

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             var personList=GetPersonList().AsParallel() 
 6                    .Where(x=>x.Age>30);
 7             Console.ReadKey();
 8         }
 9 
10         //模拟源資料
11         static IList<Person> GetPersonList()
12         {
13             var personList = new List<Person>();
14 
15             var person1 = new Person();
16             person1.ID = 1;
17             person1.Name = "Leslie";
18             person1.Age = 30;
19             personList.Add(person1);
20             ...........
21             return personList;
22         }
23     }      
C# 多線程

7.4.2 AsOrdered

若要使查詢結果必須保留源序列排序方式,可以使用AsOrdered方法。 

AsOrdered依然使用并行方式,隻是在查詢過程加入額外資訊,在并行結束後把查詢結果再次進行排列。

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             var personList=GetPersonList().AsParallel().AsOrdered()
 6                 .Where(x=>x.Age<30);
 7             Console.ReadKey();
 8         }
 9 
10         static IList<Person> GetPersonList()
11         {......}
12     }      
C# 多線程

7.4.3 WithDegreeOfParallelism

預設情況下,PLINQ 使用主機上的所有處理器,這些處理器的數量最多可達 64 個。

通過使用 WithDegreeOfParallelism(Of TSource) 方法,可以訓示 PLINQ 使用不多于指定數量的處理器。

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             var personList=GetPersonList().AsParallel().WithDegreeOfParallelism(2)
 6                 .Where(x=>x.Age<30);
 7             Console.ReadKey();
 8         }
 9 
10         static IList<Person> GetPersonList()
11         {.........}
12     }      
C# 多線程

7.4.4 ForAll

如果要對并行查詢結果進行操作,一般會在for或foreach中執行,執行枚舉操作時會使用同步方式。

有見及此,PLINQ中包含了ForAll方法,它可以使用并行方式對資料集進行操作。

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             ThreadPool.SetMaxThreads(1000, 1000);
 6             GetPersonList().AsParallel().ForAll(person =>{
 7                 ThreadPoolMessage(person);
 8             });
 9             Console.ReadKey();
10         }
11 
12         static IList<Person> GetPersonList()
13         {.......}
14 
15          //顯示線程池現狀
16         static void ThreadPoolMessage(Person person)
17         {
18             int a, b;
19             ThreadPool.GetAvailableThreads(out a, out b);
20             string message = string.Format("Person  ID:{0} Name:{1} Age:{2}\n" +
21                   "  CurrentThreadId is {3}\n  WorkerThreads is:{4}" +
22                   "  CompletionPortThreads is :{5}\n",
23                   person.ID, person.Name, person.Age,
24                   Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
25             Console.WriteLine(message);
26         }
27     }      
C# 多線程
C# 多線程

7.4.5 WithCancellation

如果需要停止查詢,可以使用 WithCancellation(Of TSource) 運算符并提供 CancellationToken 執行個體作為參數。 

與第三節Task的例子相似,如果标記上的 IsCancellationRequested 屬性設定為 true,則 PLINQ 将會注意到它,并停止所有線程上的處理,然後引發 OperationCanceledException。這可以保證并行查詢能夠立即停止。

C# 多線程
1     class Program
 2     {
 3         static CancellationTokenSource tokenSource = new CancellationTokenSource();
 4 
 5         static void Main(string[] args)
 6         {
 7             Task.Factory.StartNew(Cancel);
 8             try
 9             {
10                 GetPersonList().AsParallel().WithCancellation(tokenSource.Token)
11                     .ForAll(person =>
12                     {
13                         ThreadPoolMessage(person);
14                     });
15             }
16             catch (OperationCanceledException ex)
17             { }
18             Console.ReadKey();
19         }
20 
21         //在10~50毫秒内發出停止信号
22         static void Cancel()
23         {
24             Random random = new Random();
25             Thread.Sleep(random.Next(10,50));
26             tokenSource.Cancel();
27         }
28 
29         static IList<Person> GetPersonList()
30         {......}
31 
32         //顯示線程池現狀
33         static void ThreadPoolMessage(Person person)
34         {
35             int a, b;
36             ThreadPool.GetAvailableThreads(out a, out b);
37             string message = string.Format("Person  ID:{0} Name:{1} Age:{2}\n" +
38                   "  CurrentThreadId is {3}\n  WorkerThreads is:{4}" +
39                   "  CompletionPortThreads is :{5}\n",
40                   person.ID, person.Name, person.Age,
41                   Thread.CurrentThread.ManagedThreadId, a.ToString(), b.ToString());
42             Console.WriteLine(message);
43         }
44     }
45                
C# 多線程

八、定時器與鎖

8.1定時器

若要長期定時進行一些工作,比如像郵箱更新,實時收聽資訊等等,可以利用定時器Timer進行操作。

在System.Threading命名空間中存在Timer類與對應的TimerCallback委托,它可以在背景線程中執行一些長期的定時操作,使主線程不受幹擾。

Timer類中最常用的構造函數為 public Timer( timerCallback , object , int , int )

timerCallback委托可以綁定執行方法,執行方法必須傳回void,它可以是無參數方法,也可以帶一個object參數的方法。

第二個參數是為 timerCallback 委托輸入的參數對象。

第三個參數是開始執行前等待的時間。

第四個參數是每次執行之間的等待時間。

開發執行個體

C# 多線程
1     class Program
 2     {
 3         static void Main(string[] args)
 4         {
 5             ThreadPool.SetMaxThreads(1000, 1000);
 6 
 7             TimerCallback callback = new TimerCallback(ThreadPoolMessage);
 8             Timer t = new Timer(callback,"Hello Jack! ", 0, 1000);
 9             Console.ReadKey();
10         }
11 
12         //顯示線程池現狀
13         static void ThreadPoolMessage(object data)
14         {
15             int a, b;
16             ThreadPool.GetAvailableThreads(out a, out b);
17             string message = string.Format("{0}\n   CurrentThreadId is:{1}\n" +
18                 "   CurrentThread IsBackground:{2}\n" +
19                 "   WorkerThreads is:{3}\n   CompletionPortThreads is:{4}\n",
20                  data + "Time now is " + DateTime.Now.ToLongTimeString(),
21                  Thread.CurrentThread.ManagedThreadId,
22                  Thread.CurrentThread.IsBackground.ToString(),
23                  a.ToString(), b.ToString());
24             Console.WriteLine(message);
25         }
26     }      
C# 多線程

注意觀察運作結果,每次調用Timer綁定的方法時不一定是使用同一線程,但線程都會是來自工作者線程的背景線程。

C# 多線程

8.2 鎖

在使用多線程開發時,存在一定的共用資料,為了避免多線程同時操作同一資料,.NET提供了lock、Monitor、Interlocked等多個鎖定資料的方式。

8.2.1 lock

lock的使用比較簡單,如果需要鎖定某個對象時,可以直接使用lock(this)的方式。

C# 多線程
1 private void Method()
2 {
3       lock(this)
4       {
5           //在此進行的操作能保證在同一時間内隻有一個線程對此對象操作
6       }
7 }      
C# 多線程

如果操作隻鎖定某段代碼,可以事先建立一個object對象,并對此對象進行操作鎖定,這也是.net提倡的鎖定用法。

C# 多線程
1 class Control
 2 {
 3       private object obj=new object();
 4       
 5       public void Method()
 6       {
 7             lock(obj)
 8             {.......}
 9       }
10 }      
C# 多線程

8.2.2 Montior

Montior存在于System.Thread命名空間内,相比lock,Montior使用更靈活。

它存在 Enter, Exit 兩個方法,它可以對對象進行鎖定與解鎖,比lock使用更靈活。

C# 多線程
1 class Control
 2 {
 3       private object obj=new object();
 4  
 5       public void Method()
 6       {
 7             Monitor.Enter(obj);
 8             try
 9             {......}
10             catch(Excetion ex)
11             {......}
12             finally
13             {
14                 Monitor.Exit(obj);
15             }
16       }
17 }
18                
C# 多線程

使用try的方式,能確定程式不會因死鎖而釋放出異常!

而且在finally中釋放obj對象能夠確定無論是否出現死鎖狀态,系統都會釋放obj對象。

而且Monitor中還存在Wait方法可以讓線程等待一段時間,然後在完成時使用Pulse、PulseAll等方法通知等待線程。

8.2.3 Interlocked

Interlocked存在于System.Thread命名空間内,它的操作比Monitor使用更簡單。

它存在CompareExchange、Decrement、Exchange、Increment等常用方法讓參數在安全的情況進行資料交換。

Increment、Decrement 可以使參數安全地加1或減1并傳回遞增後的新值。

C# 多線程
1 class Example
2 {
3       private int a=1;
4 
5       public void AddOne()
6       {
7              int newA=Interlocked.Increment(ref a);
8       }
9 }      
C# 多線程

Exchange可以安全地變量指派。

1 public void SetData()
2 {
3       Interlocked.Exchange(ref a,100);
4 }      

CompareExchange使用特别友善,它相當于if的用法,當a等于1時,則把100指派給a。

1 public void CompareAndExchange()
2 {
3     Interlocked.CompareExchange(ref a,100,1);
4 }      

結束語

熟悉掌握多線程開發,對提高系統工作效率非常有幫助,尤其是回調方法與最近火熱的并行程式設計更應該引起各位的重視。

通路我

C# 多線程
C# 多線程

關注天雲

定期福利