C#多執行緒(轉載 來自網路)
一.多執行緒的概念
Windows是一個多工的系統,如果你使用的是windows 2000及 其以上版本,你可以通過工作管理員檢視當前系統執行的程式和程序。什麼是程序呢?當一個程式開始執行時,它就是一個程序,程序所指包括執行中的程式和程式所使用到的記憶體和系統資源。而一個程序又是由多個執行緒所組成的,執行緒是程式中的一個執行流,每個執行緒都有自己的專有暫存器(棧指標、程式計數器等),但程式碼區是共享的,即不同的執行緒可以執行同樣的函式。多執行緒是指程式中包含多個執行流,即在一個程式中可以同時執行多個不同的執行緒來執行不同的任務,也就是說允許單個程式建立多個並行執行的執行緒來完成各自的任務。瀏覽器就是一個很好的多執行緒的例子,在瀏覽器中你可以在下載JAVA小應用程式或圖象的同時滾動頁面,在訪問新頁面時,播放動畫和聲音,列印檔案等。
多執行緒的好處在於可以提高CPU的利用率——任何一個程式設計師都不希望自己的程式很多時候沒事可幹,在多執行緒程式中,一個執行緒必須等待的時候,CPU可以執行其它的執行緒而不是等待,這樣就大大提高了程式的效率。
然而我們也必須認識到執行緒本身可能影響系統性能的不利方面,以正確使用執行緒:
- 執行緒也是程式,所以執行緒需要佔用記憶體,執行緒越多佔用記憶體也越多
- 多執行緒需要協調和管理,所以需要CPU時間跟蹤執行緒
- 執行緒之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題
- 執行緒太多會導致控制太複雜,最終可能造成很多Bug
本文將對C#程式設計中的多執行緒機制進行探討,通過一些例項解決對執行緒的控制,多執行緒間通訊等問題。為了省去建立GUI那些繁瑣的步驟,更清晰地逼近執行緒的本質,下面所有的程式都是控制檯程式,程式最後的Console.ReadLine()是為了使程式中途停下來,以便看清楚執行過程中的輸出。
好了,廢話少說,讓我們來體驗一下多執行緒的C#吧!
二.操縱一個執行緒
任何程式在執行時,至少有一個主執行緒,下面這段小程式可以給讀者一個直觀的印象://SystemThread.cs
using System;
using System.Threading;
namespace ThreadTest
{
class RunIt
{
[STAThread]
static void Main(string[] args)
{
Thread.CurrentThread.Name="SystemThread";//給當前執行緒起名為"SystemThread"
Console.WriteLine(Thread.CurrentThread.Name+"'Status:"+Thread.CurrentThread.ThreadState);
Console.ReadLine();
}
}
}
編譯執行後你看到了什麼?是的,程式將產生如下輸出:
System Thread's Status:Running
在這裡,我們通過Thread類的靜態屬性CurrentThread獲取了當前執行的執行緒,對其Name屬性賦值“System Thread”,最後還輸出了它的當前狀態(ThreadState)。所謂靜態屬性,就是這個類所有物件所公有的屬性,不管你建立了多少個這個類的例項,但是類的靜態屬性在記憶體中只有一個。很容易理解CurrentThread為什麼是靜態的——雖然有多個執行緒同時存在,但是在某一個時刻,CPU只能執行其中一個。
就像上面程式所演示的,我們通過Thread類來建立和控制執行緒。注意到程式的頭部,我們使用瞭如下名稱空間:
using System;
using System.Threading;
在.net framework class library中,所有與多執行緒機制應用相關的類都是放在System.Threading名稱空間中的。其中提供Thread類用於建立執行緒,ThreadPool類用於管理執行緒池等等,此外還提供解決了執行緒執行安排,死鎖,執行緒間通訊等實際問題的機制。如果你想在你的應用程式中使用多執行緒,就必須包含這個類。
Thread類有幾個至關重要的方法,描述如下:
- Start():啟動執行緒
- Sleep(int):靜態方法,暫停當前執行緒指定的毫秒數
- Abort():通常使用該方法來終止一個執行緒
- Suspend():該方法並不終止未完成的執行緒,它僅僅掛起執行緒,以後還可恢復。
- Resume():恢復被Suspend()方法掛起的執行緒的執行
下面我們就動手來建立一個執行緒,使用Thread類建立執行緒時,只需提供執行緒入口即可。執行緒入口使程式知道該讓這個執行緒幹什麼事,在C#中,執行緒入口是通過ThreadStart代理(delegate)來提供的,你可以把ThreadStart理解為一個函式指標,指向執行緒要執行的函式,當呼叫Thread.Start()方法後,執行緒就開始執行ThreadStart所代表或者說指向的函式。
開啟你的VS.net,新建一個控制檯應用程式(Console Application),下面這些程式碼將讓你體味到完全控制一個執行緒的無窮樂趣! //ThreadTest.cs
using System;
using System.Threading;
namespace ThreadTest
{
public class Alpha
{
public void Beta()
{
while (true)
{
Console.WriteLine("Alpha.Beta is runningin its own thread.");
}
}
};
public class Simple
{
public static int Main()
{
Console.WriteLine("Thread Start/Stop/JoinSample");
Alpha oAlpha = new Alpha();
file://這裡建立一個執行緒,使之執行Alpha類的Beta()方法
Thread oThread = new Thread(newThreadStart(oAlpha.Beta));
oThread.Start();
while (!oThread.IsAlive);
Thread.Sleep(1);
oThread.Abort();
oThread.Join();
Console.WriteLine();
Console.WriteLine("Alpha.Beta hasfinished");
try
{
Console.WriteLine("Try to restart theAlpha.Beta thread");
oThread.Start();
}
catch (ThreadStateException)
{
Console.Write("ThreadStateExceptiontrying to restart Alpha.Beta. ");
Console.WriteLine("Expected sinceaborted threads cannot be restarted.");
Console.ReadLine();
}
return 0;
}
}
}
這段程式包含兩個類Alpha和Simple,在建立執行緒oThread時我們用指向Alpha.Beta()方法的初始化了ThreadStart代理(delegate)物件,當我們建立的執行緒oThread呼叫oThread.Start()方法啟動時,實際上程式執行的是Alpha.Beta()方法:
Alpha oAlpha = new Alpha();
Thread oThread = new Thread(newThreadStart(oAlpha.Beta));
oThread.Start();
然後在Main()函式的while迴圈中,我們使用靜態方法Thread.Sleep()讓主執行緒停了1ms,這段時間CPU轉向執行執行緒oThread。然後我們試圖用Thread.Abort()方法終止執行緒oThread,注意後面的oThread.Join(),Thread.Join()方法使主執行緒等待,直到oThread執行緒結束。你可以給Thread.Join()方法指定一個int型的引數作為等待的最長時間。之後,我們試圖用Thread.Start()方法重新啟動執行緒oThread,但是顯然Abort()方法帶來的後果是不可恢復的終止執行緒,所以最後程式會丟擲ThreadStateException異常。
程式最後得到的結果將如下圖:
在這裡我們要注意的是其它執行緒都是依附於Main()函式所在的執行緒的,Main()函式是C#程式的入口,起始執行緒可以稱之為主執行緒,如果所有的前臺執行緒都停止了,那麼主執行緒可以 終止,而所有的後臺執行緒都將無條件終止。而所有的執行緒雖然在微觀上是序列執行的,但是在巨集觀上你完全可以認為它們在並行執行。
讀者一定注意到了Thread.ThreadState這個屬性,這個屬性代表了執行緒執行時狀態,在不同的情況下有不同的值,於是我們有時候可以通過對該值的判斷來設計程式流程。ThreadState在各種情況下的可能取值如下:
- Aborted:執行緒已停止
- AbortRequested:執行緒的Thread.Abort()方法已被呼叫,但是執行緒還未停止
- Background:執行緒在後臺執行,與屬性Thread.IsBackground有關
- Running:執行緒正在正常執行
- Stopped:執行緒已經被停止
- StopRequested:執行緒正在被要求停止
- Suspended:執行緒已經被掛起(此狀態下,可以通過呼叫Resume()方法重新執行)
- SuspendRequested:執行緒正在要求被掛起,但是未來得及響應
- Unstarted:未呼叫Thread.Start()開始執行緒的執行
- WaitSleepJoin:執行緒因為呼叫了Wait(),Sleep()或Join()等方法處於封鎖狀態
上面提到了Background狀態表示該執行緒在後臺執行,那麼後臺執行的執行緒有什麼特別的地方呢?其實後臺執行緒跟前臺執行緒只有一個區別,那就是後臺執行緒不妨礙程式的終止。一旦一個程序所有的前臺執行緒都終止後,CLR(通用語言執行環境)將通過呼叫任意一個存活中的後臺程序的Abort()方法來徹底終止程序。
當執行緒之間爭奪CPU時間時,CPU按照是執行緒的優先順序給予服務的。在C#應用程式中,使用者可以設定5個不同的優先順序,由高到低分別是Highest,AboveNormal,Normal,BelowNormal,Lowest,在建立執行緒時如果不指定優先順序,那麼系統預設為ThreadPriority.Normal。給一個執行緒指定優先順序
,我們可以使用如下程式碼: //設定優先順序為最低
myThread.Priority=ThreadPriority.Lowest;
通過設定執行緒的優先順序,我們可以安排一些相對重要的執行緒優先執行,例如對使用者的響應等等。
現在我們對怎樣建立和控制一個執行緒已經有了一個初步的瞭解,下面我們將深入研究執行緒實現中比較典型的的問題,並且探討其解決方法。
三.執行緒的同步和通訊——生產者和消費者
假設這樣一種情況,兩個執行緒同時維護一個佇列,如果一個執行緒對佇列中新增元素,而另外一個執行緒從佇列中取用元素,那麼我們稱新增元素的執行緒為生產者,稱取用 元素的執行緒為消費者。生產者與消費者問題看起來很簡單,但是卻是多執行緒應用中一個必須解決的問題,它涉及到執行緒之間的同步和通訊問題。
前面說過,每個執行緒都有自己的資源,但是程式碼區是共享的,即每個執行緒都可以執行相同的函式。但是多執行緒環境下,可能帶來的問題就是幾個執行緒同時執行一個函式,導致資料的混亂,產生不可預料的結果,因此我們必須避免這種情況的發生。C#提供了一個關鍵字lock,它可以把一段程式碼定義為互斥段(critical section),互斥段在一個時刻內只允許一個執行緒進入執行,而其他執行緒必須等待。在C#中,關鍵字lock定義如下:
lock(expression) statement_blockexpression代表你希望跟蹤的物件,通常是物件引用。一般地,如果你想保護一個類的例項,你可以使用this;如果你希望保護一個靜態變數(如互斥程式碼段在一個靜態方法內部),一般使用類名就可以了。而statement_block就是互斥段的程式碼,這段程式碼在一個時刻內只可能被一個執行緒執行。
下面是一個使用lock關鍵字的典型例子,我將在註釋裡向大家說明lock關鍵字的用法和用途: //lock.cs
using System;
using System.Threading;
internal class Account
{
int balance;
Random r = new Random();
internal Account(int initial)
{
balance = initial;
}
internal int Withdraw(intamount)
{
if (balance < 0)
{
file://如果balance小於0則丟擲異常
throw newException("Negative Balance");
}
//下面的程式碼保證在當前執行緒修改balance的值完成之前
//不會有其他執行緒也執行這段程式碼來修改balance的值
//因此,balance的值是不可能小於0的
lock (this)
{
Console.WriteLine("CurrentThread:"+Thread.CurrentThread.Name);
file://如果沒有lock關鍵字的保護,那麼可能在執行完if的條件判斷之後
file://另外一個執行緒卻執行了balance=balance-amount修改了balance的值
file://而這個修改對這個執行緒是不可見的,所以可能導致這時if的條件已經不成立了
file://但是,這個執行緒卻繼續執行balance=balance-amount,所以導致balance可能小於0
if (balance >= amount)
{
Thread.Sleep(5);
balance = balance - amount;
return amount;
}
else
{
return 0; // transactionrejected
}
}
}
internal void DoTransactions()
{
for (int i = 0; i < 100; i++)
Withdraw(r.Next(-50, 100));
}
}
internal class Test
{
static internal Thread[] threads= new Thread[10];
public static void Main()
{
Account acc = new Account (0);
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(newThreadStart(acc.DoTransactions));
threads[i] = t;
}
for (int i = 0; i < 10; i++)
threads[i].Name=i.ToString();
for (int i = 0; i < 10; i++)
threads[i].Start();
Console.ReadLine();
}
}
而多執行緒公用一個物件時,也會出現和公用程式碼類似的問題,這種問題就不應該使用lock關鍵字了,這裡需要用到System.Threading中的一個類Monitor,我們可以稱之為監視器,Monitor提供了使執行緒共享資源的方案。
Monitor類可以鎖定一個物件,一個執行緒只有得到這把鎖才可以對該物件進行操作。物件鎖機制保證了在可能引起混亂的情況下一個時刻只有一個執行緒可以訪問這個物件。Monitor必須和一個具體的物件相關聯,但是由於它是一個靜態的類,所以不能使用它來定義物件,而且它的所有方法都是靜態的,不能使用物件來引用。下面程式碼說明了使用Monitor鎖定一個物件的情形: ......
Queue oQueue=new Queue();
......
Monitor.Enter(oQueue);
......//現在oQueue物件只能被當前執行緒操縱了
Monitor.Exit(oQueue);//釋放鎖
如上所示,當一個執行緒呼叫Monitor.Enter()方法鎖定一個物件時,這個物件就歸它所有了,其它執行緒想要訪問這個物件,只有等待它使用Monitor.Exit()方法釋放鎖。為了保證執行緒最終都能釋放鎖,你可以把Monitor.Exit()方法寫在try-catch-finally結構中的finally程式碼塊裡。對於任何一個被Monitor鎖定的物件,記憶體中都儲存著與它相關的一些資訊,其一是現在持有鎖的執行緒的引用,其二是一個預備佇列,佇列中儲存了已經準備好獲取鎖的執行緒,其三是一個等待佇列,佇列中儲存著當前正在等待這個物件狀態改變的佇列的引用。當擁有物件鎖的執行緒準備釋放鎖時,它使用Monitor.Pulse()方法通知等待佇列中的第一個執行緒,於是該執行緒被轉移到預備佇列中,當物件鎖被釋放時,在預備佇列中的執行緒可以立即獲得物件鎖。
下面是一個展示如何使用lock關鍵字和Monitor類來實現執行緒的同步和通訊的例子,也是一個典型的生產者與消費者問題。這個例程中,生產者執行緒和消費者執行緒是交替進行的,生產者寫入一個數,消費者立即讀取並且顯示,我將在註釋中介紹該程式的精要所在。用到的系統名稱空間如下:
using System;
using System.Threading;
首先,我們定義一個被操作的物件的類Cell,在這個類裡,有兩個方法:ReadFromCell()和WriteToCell。消費者執行緒將呼叫ReadFromCell()讀取cellContents的內容並且顯示出來,生產者程序將呼叫WriteToCell()方法向cellContents寫入資料。
public class Cell
{
int cellContents; // Cell物件裡邊的內容
bool readerFlag = false; // 狀態標誌,為true時可以讀取,為false則正在寫入
public int ReadFromCell( )
{
lock(this) // Lock關鍵字保證了什麼,請大家看前面對lock的介紹
{
if (!readerFlag)//如果現在不可讀取
{
try
{
file://等待WriteToCell方法中呼叫Monitor.Pulse()方法
Monitor.Wait(this);
}
catch(SynchronizationLockException e)
{
Console.WriteLine(e);
}
catch(ThreadInterruptedException e)
{
Console.WriteLine(e);
}
}
Console.WriteLine("Consume:{0}",cellContents);
readerFlag = false; file://重置readerFlag標誌,表示消費行為已經完成
Monitor.Pulse(this); file://通知WriteToCell()方法(該方法在另外一個執行緒中執行,等待中)
}
return cellContents;
}
public void WriteToCell(int n)
{
lock(this)
{
if (readerFlag)
{
try
{
Monitor.Wait(this);
}
catch(SynchronizationLockException e)
{
file://當同步方法(指Monitor類除Enter之外的方法)在非同步的程式碼區被呼叫
Console.WriteLine(e);
}
catch(ThreadInterruptedException e)
{
file://當執行緒在等待狀態的時候中止
Console.WriteLine(e);
}
}
cellContents = n;
Console.WriteLine("Produce:{0}",cellContents);
readerFlag = true;
Monitor.Pulse(this); file://通知另外一個執行緒中正在等待的ReadFromCell()方法
}
}
}
下面定義生產者CellProd和消費者類CellCons,它們都只有一個方法ThreadRun(),以便在Main()函式中提供給執行緒的ThreadStart代理物件,作為執行緒的入口。
public class CellProd
{
Cell cell; // 被操作的Cell物件
int quantity = 1; // 生產者生產次數,初始化為1
public CellProd(Cell box, intrequest)
{
//建構函式
cell = box;
quantity = request;
}
public void ThreadRun( )
{
for(int looper=1;looper<=quantity; looper++)
cell.WriteToCell(looper);file://生產者向操作物件寫入資訊
}
}
public class CellCons
{
Cell cell;
int quantity = 1;
public CellCons(Cell box, intrequest)
{
cell = box;
quantity = request;
}
public void ThreadRun( )
{
int valReturned;
for(int looper=1;looper<=quantity; looper++)
valReturned=cell.ReadFromCell();//消費者從操作物件中讀取資訊
}
}
然後在下面這個類MonitorSample的Main()函式中我們要做的就是建立兩個執行緒分別作為生產者和消費者,使用CellProd.ThreadRun()方法和CellCons.ThreadRun()方法對同一個Cell物件進行操作。
public class MonitorSample
{
public static void Main(String[]args)
{
int result = 0; file://一個標誌位,如果是0表示程式沒有出錯,如果是1表明有錯誤發生
Cell cell = new Cell( );
//下面使用cell初始化CellProd和CellCons兩個類,生產和消費次數均為20次
CellProd prod = newCellProd(cell, 20);
CellCons cons = newCellCons(cell, 20);
Thread producer = new Thread(newThreadStart(prod.ThreadRun));
Thread consumer = new Thread(newThreadStart(cons.ThreadRun));
//生產者執行緒和消費者執行緒都已經被建立,但是沒有開始執行
try
{
producer.Start( );
consumer.Start( );
producer.Join( );
consumer.Join( );
Console.ReadLine();
}
catch (ThreadStateException e)
{
file://當執行緒因為所處狀態的原因而不能執行被請求的操作
Console.WriteLine(e);
result = 1;
}
catch(ThreadInterruptedException e)
{
file://當執行緒在等待狀態的時候中止
Console.WriteLine(e);
result = 1;
}
//儘管Main()函式沒有返回值,但下面這條語句可以向父程序返回執行結果
Environment.ExitCode = result;
}
}
大家可以看到,在上面的例程中,同步是通過等待Monitor.Pulse()來完成的。首先生產者生產了一個值,而同一時刻消費者處於等待狀態,直到收到生產者的“脈衝(Pulse)”通知它生產已經完成,此後消費者進入消費狀態,而生產者開始等待消費者完成操作後將呼叫Monitor.Pulese()發出的“脈衝”。它的執行結果很簡單:
Produce: 1
Consume: 1
Produce: 2
Consume: 2
Produce: 3
Consume: 3
...
...
Produce: 20
Consume: 20
事實上,這個簡單的例子已經幫助我們解決了多執行緒應用程式中可能出現的大問題,只要領悟瞭解決執行緒間衝突的基本方法,很容易把它應用到比較複雜的程式中去。
四、執行緒池和定時器——多執行緒的自動管理
在多執行緒的程式中,經常會出現兩種情況。一種情況下,應用程式中的執行緒把大部分的時間花費在等待狀態,等待某個事件發生,然後才能給予響應;而另外一種情況則是執行緒平常都處於休眠狀態,只是週期性地被喚醒。在.net framework裡邊,我們使用ThreadPool來對付第一種情況,使用Timer來對付第二種情況。
ThreadPool類提供一個由系統維護的執行緒池——可以看作一個執行緒的容器,該容器需要Windows 2000以上版本的系統支援,因為其中某些方法呼叫了只有高版本的Windows才有的API函式。你可以使用ThreadPool.QueueUserWorkItem()方法將執行緒安放線上程池裡,該方法的原型如下:
//將一個執行緒放進執行緒池,該執行緒的Start()方法將呼叫WaitCallback代理物件代表的函式
public static boolQueueUserWorkItem(WaitCallback);
//過載的方法如下,引數object將傳遞給WaitCallback所代表的方法
public static boolQueueUserWorkItem(WaitCallback, object);
要注意的是,ThreadPool類也是一個靜態類,你不能也不必要生成它的物件,而且一旦使用該方法線上程池中添加了一個專案,那麼該專案將是沒有辦法取消的。在這裡你無需自己建立執行緒,只需把你要做的工作寫成函式,然後作為引數傳遞給ThreadPool.QueueUserWorkItem()方法就行了,傳遞的方法就是依靠WaitCallback代理物件,而執行緒的建立、管理、執行等等工作都是由系統自動完成的,你無須考慮那些複雜的細節問題,執行緒池的優點也就在這裡體現出來了,就好像你是公司老闆——只需要安排工作,而不必親自動手。下面的例程演示了ThreadPool的用法。首先程式建立了一個ManualResetEvent物件,該物件就像一個訊號燈,可以利用它的訊號來通知其它執行緒,本例中當執行緒池中所有執行緒工作都完成以後,ManualResetEvent的物件將被設定為有訊號,從而通知主執行緒繼續執行。它有幾個重要的方法:Reset(),Set(),WaitOne()。初始化該物件時,使用者可以指定其預設的狀態(有訊號/無訊號),在初始化以後,該物件將保持原來的狀態不變直到它的Reset()或者Set()方法被呼叫,Reset()方法將其設定為無訊號狀態,Set()方法將其設定為有訊號狀態。WaitOne()方法使當前執行緒掛起直到ManualResetEvent物件處於有訊號狀態,此時該執行緒將被啟用。然後,程式將向執行緒池中新增工作項,這些以函式形式提供的工作項被系統用來初始化自動建立的執行緒。當所有的執行緒都執行完了以後,ManualResetEvent.Set()方法被呼叫,因為呼叫了ManualResetEvent.WaitOne()方法而處在等待狀態的主執行緒將接收到這個訊號,於是它接著往下執行,完成後邊的工作。
using System;
using System.Collections;
using System.Threading;
//這是用來儲存資訊的資料結構,將作為引數被傳遞
public class SomeState{
public int Cookie;
public SomeState(int iCookie)
{
Cookie = iCookie;
}
}
public class Alpha
{
public Hashtable HashCount;
public ManualResetEvent eventX;
public static int iCount = 0;
public static int iMaxCount = 0;
public Alpha(int MaxCount)
{
HashCount = newHashtable(MaxCount);
iMaxCount = MaxCount;
}
file://執行緒池裡的執行緒將呼叫Beta()方法
public void Beta(Object state)
{
//輸出當前執行緒的hash編碼值和Cookie的值
Console.WriteLine(" {0} {1}:", Thread.CurrentThread.GetHashCode(),
((SomeState)state).Cookie);
Console.WriteLine("HashCount.Count=={0},Thread.CurrentThread.GetHashCode()=={1}", HashCount.Count, Thread.CurrentThread.GetHashCode());
lock (HashCount)
{
file://如果當前的Hash表中沒有當前執行緒的Hash值,則新增之
if(!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode()))
HashCount.Add(Thread.CurrentThread.GetHashCode(), 0);
HashCount[Thread.CurrentThread.GetHashCode()]=
((int)HashCount[Thread.CurrentThread.GetHashCode()])+1;
}
int iX = 2000;
Thread.Sleep(iX);
//Interlocked.Increment()操作是一個原子操作,具體請看下面說明
Interlocked.Increment(refiCount);
if (iCount == iMaxCount)
{
Console.WriteLine();
Console.WriteLine("SettingeventX ");
eventX.Set();
}
}
}
public class SimplePool
{
public static int Main(string[]args)
{
Console.WriteLine("ThreadPool Sample:");
bool W2K = false;
int MaxCount = 10;//允許執行緒池中執行最多10個執行緒
//新建ManualResetEvent物件並且初始化為無訊號狀態
ManualResetEvent eventX = newManualResetEvent(false);
Console.WriteLine("Queuing{0} items to Thread Pool", MaxCount);
Alpha oAlpha = newAlpha(MaxCount); file://建立工作項
//注意初始化oAlpha物件的eventX屬性
oAlpha.eventX = eventX;
Console.WriteLine("Queue toThread Pool 0");
try
{
file://將工作項裝入執行緒池
file://這裡要用到Windows 2000以上版本才有的API,所以可能出現NotSupportException異常
ThreadPool.QueueUserWorkItem(newWaitCallback(oAlpha.Beta),
new SomeState(0));
W2K = true;
}
catch (NotSupportedException)
{
Console.WriteLine("TheseAPI's may fail when called on a non-Windows 2000 system.");
W2K = false;
}
if (W2K)//如果當前系統支援ThreadPool的方法.
{
for (int iItem=1;iItem <MaxCount;iItem++)
{
//插入佇列元素
Console.WriteLine("Queue toThread Pool {0}", iItem);
ThreadPool.QueueUserWorkItem(newWaitCallback(oAlpha.Beta),new SomeState(iItem));
}
Console.WriteLine("Waitingfor Thread Pool to drain");
file://等待事件的完成,即執行緒呼叫ManualResetEvent.Set()方法
eventX.WaitOne(Timeout.Infinite,true);
file://WaitOne()方法使呼叫它的執行緒等待直到eventX.Set()方法被呼叫
Console.WriteLine("ThreadPool has been drained (Event fired)");
Console.WriteLine();
Console.WriteLine("Loadacross threads");
foreach(object o inoAlpha.HashCount.Keys)
Console.WriteLine("{0}{1}", o, oAlpha.HashCount[o]);
}
Console.ReadLine();
return 0;
}
}
程式中有些小地方應該引起我們的注意。SomeState類是一個儲存資訊的資料結構,在上面的程式中,它作為引數被傳遞給每一個執行緒,你很容易就能理解這個,因為你需要把一些有用的資訊封裝起來提供給執行緒,而這種方式是非常有效的。程式出現的InterLocked類也是專為多執行緒程式而存在的,它提供了一些有用的原子操作,所謂原子操作就是在多執行緒程式中,如果這個執行緒呼叫這個操作修改一個變數,那麼其他執行緒就不能修改這個變量了,這跟lock關鍵字在本質上是一樣的。
我們應該徹底地分析上面的程式,把握住執行緒池的本質,理解它存在的意義是什麼,這樣我們才能得心應手地使用它。下面是該程式的輸出結果: Thread Pool Sample:
Queuing 10 items to Thread Pool
Queue to Thread Pool 0
Queue to Thread Pool 1
...
...
Queue to Thread Pool 9
Waiting for Thread Pool to drain
98 0 :
HashCount.Count==0,Thread.CurrentThread.GetHashCode()==98
100 1 :
HashCount.Count==1, Thread.CurrentThread.GetHashCode()==100
98 2 :
...
...
Setting eventX
Thread Pool has been drained(Event fired)
Load across threads
101 2
100 3
98 4
102 1
與ThreadPool類不同,Timer類的作用是設定一個定時器,定時執行使用者指定的函式,而這個函式的傳遞是靠另外一個代理物件TimerCallback,它必須在建立Timer物件時就指定,並且不能更改。定時器啟動後,系統將自動建立一個新的執行緒,並且在這個執行緒裡執行使用者指定的函式。下面的語句初始化了一個Timer物件: Timer timer = new Timer(timerDelegate,s,1000, 1000);
第一個引數指定了TimerCallback代理物件;第二個引數的意義跟上面提到的WaitCallback代理物件的一樣,作為一個傳遞資料的物件傳遞給要呼叫的方法;第三個引數是延遲時間——計時開始的時刻距現在的時間,單位是毫秒;第四個引數是定時器的時間間隔——計時開始以後,每隔這麼長的一段時間,TimerCallback所代表的方法將被呼叫一次,單位也是毫秒。這句話的意思就是將定時器的延遲時間和時間間隔都設為1秒鐘。
定時器的設定是可以改變的,只要呼叫Timer.Change()方法,這是一個引數型別過載的方法,一般使用的原型如下: public bool Change(long, long);
下面這段程式碼將前邊設定的定時器修改了一下: timer.Change(10000,2000);
很顯然,定時器timer的時間間隔被重新設定為2秒,停止計時10秒後生效。
下面這段程式演示了Timer類的用法。
using System;
using System.Threading;
class TimerExampleState
{
public int counter = 0;
public Timer tmr;
}
class App
{
public static void Main()
{
TimerExampleState s = new TimerExampleState();
//建立代理物件TimerCallback,該代理將被定時呼叫
TimerCallback timerDelegate =new TimerCallback(CheckStatus);
//建立一個時間間隔為1s的定時器
Timer timer = newTimer(timerDelegate, s,1000, 1000);
s.tmr = timer;
//主執行緒停下來等待Timer物件的終止
while(s.tmr != null)
Thread.Sleep(0);
Console.WriteLine("Timerexample done.");
Console.ReadLine();
}
file://下面是被定時呼叫的方法
static void CheckStatus(Objectstate)
{
TimerExampleState s=(TimerExampleState)state;
s.counter++;
Console.WriteLine("{0}Checking Status {1}.",DateTime.Now.TimeOfDay, s.counter);
if(s.counter == 5)
{
file://使用Change方法改變了時間間隔
(s.tmr).Change(10000,2000);
Console.WriteLine("changed...");
}
if(s.counter == 10)
{
Console.WriteLine("disposingof timer...");
s.tmr.Dispose();
s.tmr = null;
}
}
}
程式首先建立了一個定時器,它將在建立1秒之後開始每隔1秒呼叫一次CheckStatus()方法,當呼叫5次以後,在CheckStatus()方法中修改了時間間隔為2秒,並且指定在10秒後重新開始。當計數達到10次,呼叫Timer.Dispose()方法刪除了timer物件,主執行緒於是跳出迴圈,終止程式。程式執行的結果如下:
上面就是對ThreadPool和Timer兩個類的簡單介紹,充分利用系統提供的功能,可以為我們省去很多時間和精力——特別是對很容易出錯的多執行緒程式。同時我們也可以看到.net Framework強大的內建物件,這些將對我們的程式設計帶來莫大的方便。、互斥物件——更加靈活的同步方式
有 時候你會覺得上面介紹的方法好像不夠用,對,我們解決了程式碼和資源的同步問題,解決了多執行緒自動化管理和定時觸發的問題,但是如何控制多個執行緒相互之間的 聯絡呢?例如我要到餐廳吃飯,在吃飯之前我先得等待廚師把飯菜做好,之後我開始吃飯,吃完我還得付款,付款方式可以是現金,也可以是信用卡,付款之後我才 能離開。分析一下這個過程,我吃飯可以看作是主執行緒,廚師做飯又是一個執行緒,服務員用信用卡收款和收現金可以看作另外兩個執行緒,大家可以很清楚地看到其中 的關係——我吃飯必須等待廚師做飯,然後等待兩個收款執行緒之中任意一個的完成,然後我吃飯這個執行緒可以執行離開這個步驟,於是我吃飯才算結束了。事實上,現實中有著比這更復雜的聯絡,我們怎樣才能很好地控制它們而不產生衝突和重複呢?
這種情況下,我們需要用到互斥物件,即System.Threading名稱空間中的Mutex類。大家一定坐過出租車吧,事實上我們可以把Mutex看作一個計程車,那麼乘客就是執行緒了,乘客首先得等車,然後上車,最後下車,當一個乘客在車上時,其他乘客就只有等他下車以後才可以上車。而執行緒與Mutex物件的關係也正是如此,執行緒使用Mutex.WaitOne()方法等待Mutex物件被釋放,如果它等待的Mutex物件被釋放了,它就自動擁有這個物件,直到它呼叫Mutex.ReleaseMutex()方法釋放這個物件,而在此期間,其他想要獲取這個Mutex物件的執行緒都只有等待。
下面這個例子使用了Mutex物件來同步四個執行緒,主執行緒等待四個執行緒的結束,而這四個執行緒的執行又是與兩個Mutex物件相關聯的。其中還用到AutoResetEvent類的物件,如同上面提到的ManualResetEvent物件一樣,大家可以把它簡單地理解為一個訊號燈,使用AutoResetEvent.Set()方法可以設定它為有訊號狀態,而使用AutoResetEvent.Reset()方法把它設定為無訊號狀態。這裡用它的有訊號狀態來表示一個執行緒的結束。
// Mutex.cs
using System;
using System.Threading;
public class MutexSample
{
static Mutex gM1;
static Mutex gM2;
const int ITERS = 100;
static AutoResetEvent Event1 =new AutoResetEvent(false);
static AutoResetEvent Event2 =new AutoResetEvent(false);
static AutoResetEvent Event3 =new AutoResetEvent(false);
static AutoResetEvent Event4 =new AutoResetEvent(false);
public static void Main(String[]args)
{
Console.WriteLine("MutexSample ...");
//建立一個Mutex物件,並且命名為MyMutex
gM1 = newMutex(true,"MyMutex");
//建立一個未命名的Mutex 物件.
gM2 = new Mutex(true);
Console.WriteLine(" - MainOwns gM1 and gM2");
AutoResetEvent[] evs = newAutoResetEvent[4];
evs[0] = Event1; file://為後面的執行緒t1,t2,t3,t4定義AutoResetEvent物件
evs[1] = Event2;
evs[2] = Event3;
evs[3] = Event4;
MutexSample tm = newMutexSample( );
Thread t1 = new Thread(newThreadStart(tm.t1Start));
Thread t2 = new Thread(newThreadStart(tm.t2Start));
Thread t3 = new Thread(newThreadStart(tm.t3Start));
Thread t4 = new Thread(newThreadStart(tm.t4Start));
t1.Start( );// 使用Mutex.WaitAll()方法等待一個Mutex陣列中的物件全部被釋放
t2.Start( );// 使用Mutex.WaitOne()方法等待gM1的釋放
t3.Start( );// 使用Mutex.WaitAny()方法等待一個Mutex陣列中任意一個物件被釋放
t4.Start( );// 使用Mutex.WaitOne()方法等待gM2的釋放
Thread.Sleep(2000);
Console.WriteLine(" - Mainreleases gM1");
gM1.ReleaseMutex( ); file://執行緒t2,t3結束條件滿足
Thread.Sleep(1000);
Console.WriteLine(" - Mainreleases gM2");
gM2.ReleaseMutex( ); file://執行緒t1,t4結束條件滿足
//等待所有四個執行緒結束
WaitHandle.WaitAll(evs);
Console.WriteLine("...Mutex Sample");
Console.ReadLine();
}
public void t1Start( )
{
Console.WriteLine("t1Startstarted, Mutex.WaitAll(Mutex[])");
Mutex[] gMs = new Mutex[2];
gMs[0] = gM1;//建立一個Mutex陣列作為Mutex.WaitAll()方法的引數
gMs[1] = gM2;
Mutex.WaitAll(gMs);//等待gM1和gM2都被釋放
Thread.Sleep(2000);
Console.WriteLine("t1Startfinished, Mutex.WaitAll(Mutex[]) satisfied");
Event1.Set( ); file://執行緒結束,將Event1設定為有訊號狀態
}
public void t2Start( )
{
Console.WriteLine("t2Startstarted, gM1.WaitOne( )");
gM1.WaitOne( );//等待gM1的釋放
Console.WriteLine("t2Startfinished, gM1.WaitOne( ) satisfied");
Event2.Set( );//執行緒結束,將Event2設定為有訊號狀態
}
public void t3Start( )
{
Console.WriteLine("t3Startstarted, Mutex.WaitAny(Mutex[])");
Mutex[] gMs = new Mutex[2];
gMs[0] = gM1;//建立一個Mutex陣列作為Mutex.WaitAny()方法的引數
gMs[1] = gM2;
Mutex.WaitAny(gMs);//等待陣列中任意一個Mutex物件被釋放
Console.WriteLine("t3Startfinished, Mutex.WaitAny(Mutex[])");
Event3.Set( );//執行緒結束,將Event3設定為有訊號狀態
}
public void t4Start( )
{
Console.WriteLine("t4Startstarted, gM2.WaitOne( )");
gM2.WaitOne( );//等待gM2被釋放
Console.WriteLine("t4Startfinished, gM2.WaitOne( )");
Event4.Set( );//執行緒結束,將Event4設定為有訊號狀態
}
}
下面是該程式的執行結果:
從執行結果可以很清楚地看到,執行緒t2,t3的執行是以gM1的釋放為條件的,而t4在gM2釋放後開始執行,t1則在gM1和gM2都被釋放了之後才執行。Main()函式最後,使用WaitHandle等待所有的AutoResetEvent物件的訊號,這些物件的訊號代表相應執行緒的結束。
六、小結
多執行緒程式設計是一個龐大的主題,而本文試圖在.net Framework環境下,使用最新的C#語言來描述多執行緒程式的概貌。希望本文能有助於大家理解執行緒這種概念,理解多執行緒的用途,理解它的C#實現方法,理解執行緒將為我們帶來的好處和麻煩。C#是一種新的語言,因此它的執行緒機制也有許多獨特的地方,希望大家能通過本文清楚地看到這些,從而可以對執行緒進行更深入的理解和探索。