.NET下多執行緒初探

iDotNetSpace發表於2009-02-23
多執行緒是許多作業系統所具有的特性,它能大大提高程式的執行效率,所以多執行緒程式設計技術為程式設計者廣泛關注。目前微軟的.Net戰略正進一步推進,各種相關的技術正為廣大程式設計者所接受,同樣在.Net中多執行緒程式設計技術具有相當重要的地位。本文我就向大家介紹在.Net下進行多執行緒程式設計的基本方法和步驟。



開始新執行緒



在.Net下建立一個新執行緒是非常容易的,你可以通過以下的語句來開始一個新的執行緒:


Thread thread = new Thread (new ThreadStart (ThreadFunc));

thread.Start ();



第一條語句建立一個新的Thread物件,並指明瞭一個該執行緒的方法。當新的執行緒開始時,該方法也就被呼叫執行了。該執行緒物件通過一個System..Threading.ThreadStart類的一個例項以型別安全的方法來呼叫它要呼叫的執行緒方法。


第二條語句正式開始該新執行緒,一旦方法Start()被呼叫,該執行緒就保持在一個"alive"的狀態下了,你可以通過讀取它的IsAlive屬性來判斷它是否處於"alive"狀態。下面的語句顯示瞭如果一個執行緒處於"alive"狀態下就將該執行緒掛起的方法:


if (thread.IsAlive) {

thread.Suspend ();

}



不過請注意,執行緒物件的Start()方法只是啟動了該執行緒,而並不保證其執行緒方法ThreadFunc()能立即得到執行。它只是保證該執行緒物件能被分配到CPU時間,而實際的執行還要由作業系統根據處理器時間來決定。


一個執行緒的方法不包含任何引數,同時也不返回任何值。它的命名規則和一般函式的命名規則相同。它既可以是靜態的(static)也可以是非靜態的(nonstatic)。當它執行完畢後,相應的執行緒也就結束了,其執行緒物件的IsAlive屬性也就被置為false了。下面是一個執行緒方法的例項:


public static void ThreadFunc()

{

for (int i = 0; i <10; i++) {

Console.WriteLine("ThreadFunc {0}", i);

}

}




前臺執行緒和後臺執行緒



.Net的公用語言執行時(Common Language Runtime,CLR)能區分兩種不同型別的執行緒:前臺執行緒和後臺執行緒。這兩者的區別就是:應用程式必須執行完所有的前臺執行緒才可以退出;而對於後臺執行緒,應用程式則可以不考慮其是否已經執行完畢而直接退出,所有的後臺執行緒在應用程式退出時都會自動結束。


一個執行緒是前臺執行緒還是後臺執行緒可由它的IsBackground屬性來決定。這個屬性是可讀又可寫的。它的預設值為false,即意味著一個執行緒預設為前臺執行緒。我們可以將它的IsBackground屬性設定為true,從而使之成為一個後臺執行緒。


下面的例子是一個控制檯程式,程式一開始便啟動了10個執行緒,每個執行緒執行5秒鐘時間。由於執行緒的IsBackground屬性預設為false,即它們都是前臺執行緒,所以儘管程式的主執行緒很快就執行結束了,但程式要到所有已啟動的執行緒都執行完畢才會結束。示例程式碼如下:


using System;

using System.Threading;

class MyApp

{

public static void Main ()

{

for (int i=0; i<10; i++) {

Thread thread = new Thread (new ThreadStart (ThreadFunc));

thread.Start ();

}

}

private static void ThreadFunc ()

{

DateTime start = DateTime.Now;

while ((DateTime.Now - start).Seconds <5)

;

}

}



接下來我們對上面的程式碼進行略微修改,將每個執行緒的IsBackground屬性都設定為true,則每個執行緒都是後臺執行緒了。那麼只要程式的主執行緒結束了,整個程式也就結束了。示例程式碼如下:


using System;

using System.Threading;

class MyApp

{

public static void Main ()

{

for (int i=0; i<10; i++) {

Thread thread = new Thread (new ThreadStart (ThreadFunc));

thread.IsBackground = true;

thread.Start ();

}

}

private static void ThreadFunc ()

{

DateTime start = DateTime.Now;

while ((DateTime.Now - start).Seconds <5)

;

}

}



既然前臺執行緒和後臺執行緒有這種差別,那麼我們怎麼知道該如何設定一個執行緒的IsBackground屬性呢?下面是一些基本的原則:對於一些在後臺執行的執行緒,當程式結束時這些執行緒沒有必要繼續執行了,那麼這些執行緒就應該設定為後臺執行緒。比如一個程式啟動了一個進行大量運算的執行緒,可是隻要程式一旦結束,那個執行緒就失去了繼續存在的意義,那麼那個執行緒就該是作為後臺執行緒的。而對於一些服務於使用者介面的執行緒往往是要設定為前臺執行緒的,因為即使程式的主執行緒結束了,其他的使用者介面的執行緒很可能要繼續存在來顯示相關的資訊,所以不能立即終止它們。這裡我只是給出了一些原則,具體到實際的運用往往需要程式設計者的進一步仔細斟酌。


執行緒優先順序



一旦一個執行緒開始執行,執行緒排程程式就可以控制其所獲得的CPU時間。如果一個託管的應用程式執行在Windows機器上,則執行緒排程程式是由Windows所提供的。在其他的平臺上,執行緒排程程式可能是作業系統的一部分,也自然可能是.Net框架的一部分。不過我們這裡不必考慮執行緒的排程程式是如何產生的,我們只要知道通過設定執行緒的優先順序我們就可以使該執行緒獲得不同的CPU時間。


執行緒的優先順序是由Thread.Priority屬性控制的,其值包含:ThreadPriority.Highest、ThreadPriority.AboveNormal、ThreadPriority.Normal、ThreadPriority.BelowNormal和ThreadPriority.Lowest。從它們的名稱上我們自然可以知道它們的優先程度,所以這裡就不多作介紹了。


執行緒的預設優先順序為ThreadPriority.Normal。理論上,具有相同優先順序的執行緒會獲得相同的CPU時間,不過在實際執行時,訊息佇列中的執行緒阻塞或是作業系統的優先順序的提高等原因會導致具有相同優先順序的執行緒會獲得不同的CPU時間。不過從總體上來考慮仍可以忽略這種差異。你可以通過以下的方法來改變一個執行緒的優先順序。


thread.Priority = ThreadPriority.AboveNormal;



或是:


thread.Priority = ThreadPriority.BelowNormal;



通過上面的第一句語句你可以提高一個執行緒的優先順序,那麼該執行緒就會相應的獲得更多的CPU時間;通過第二句語句你便降低了那個執行緒的優先順序,於是它就會被分配到比原來少的CPU時間了。你可以在一個執行緒開始執行前或是在它的執行過程中的任何時候改變它的優先順序。理論上你還可以任意的設定每個執行緒的優先順序,不過一個優先順序過高的執行緒往往會影響到其他執行緒的執行,甚至影響到其他程式的執行,所以最好不要隨意的設定執行緒的優先順序。



掛起執行緒和重新開始執行緒



Thread類分別提供了兩個方法來掛起執行緒和重新開始執行緒,也就是Thread.Suspend能暫停一個正在執行的執行緒,而Thread.Resume又能讓那個執行緒繼續執行。不像Windows核心,.Net框架是不記錄執行緒的掛起次數的,所以不管你掛起執行緒過幾次,只要一次呼叫Thread.Resume就可以讓掛起的執行緒重新開始執行。


Thread類還提供了一個靜態的Thread.Sleep方法,它能使一個執行緒自動的掛起一定的時間,然後自動的重新開始。一個執行緒能在它自身內部呼叫Thread.Sleep方法,也能在自身內部呼叫Thread.Suspend方法,可是一定要別的執行緒來呼叫它的Thread.Resume方法才可以重新開始。這一點是不是很容易想通的啊?下面的例子顯示瞭如何運用Thread.Sleep方法:


while (ContinueDrawing) {

DrawNextSlide ();

Thread.Sleep (5000);

}




終止執行緒



在託管的程式碼中,你可以通過以下的語句在一個執行緒中將另一個執行緒終止掉:


thread.Abort ();



下面我們來解釋一下Abort()方法是如何工作的。因為公用語言執行時管理了所有的託管的執行緒,同樣它能在每個執行緒內丟擲異常。Abort()方法能在目標執行緒中丟擲一個ThreadAbortException異常從而導致目標執行緒的終止。不過Abort()方法被呼叫後,目標執行緒可能並不是馬上就終止了。因為只要目標執行緒正在呼叫非託管的程式碼而且還沒有返回的話,該執行緒就不會立即終止。而如果目標執行緒在呼叫非託管的程式碼而且陷入了一個死迴圈的話,該目標執行緒就根本不會終止。不過這種情況只是一些特例,更多的情況是目標執行緒在呼叫託管的程式碼,一旦Abort()被呼叫那麼該執行緒就立即終止了。


在實際應用中,一個執行緒終止了另一個執行緒,不過往往要等那個執行緒完全終止了它才可以繼續執行,這樣的話我們就應該用到它的Join()方法。示例程式碼如下:


thread.Abort (); // 要求終止另一個執行緒

thread.Join (); // 只到另一個執行緒完全終止了,它才繼續執行



但是如果另一個執行緒一直不能終止的話(原因如前所述),我們就需要給Join()方法設定一個時間限制,方法如下:


thread.Join (5000); // 暫停5秒



這樣,在5秒後,不管那個執行緒有沒有完全終止,本執行緒就強行執行了。該方法還返回一個布林型的值,如果是true則表明那個執行緒已經完全終止了,而如果是false的話,則表明已經超過了時間限制了。


時鐘執行緒



.Net框架中的Timer類可以讓你使用時鐘執行緒,它是包含在System.Threading名字空間中的,它的作用就是在一定的時間間隔後呼叫一個執行緒的方法。下面我給大家展示一個具體的例項,該例項以1秒為時間間隔,在控制檯中輸出不同的字串,程式碼如下:


using System;

using System.Threading;

class MyApp

{

private static bool TickNext = true;

public static void Main ()

{

Console.WriteLine ("Press Enter to terminate...");

TimerCallback callback = new TimerCallback (TickTock);

Timer timer = new Timer (callback, null, 1000, 1000);

Console.ReadLine ();

}

private static void TickTock (object state)

{

Console.WriteLine (TickNext ? "Tick" : "Tock");

TickNext = ! TickNext;

}

}



從上面的程式碼中,我們知道第一個函式回撥是在1000毫秒後才發生的,以後的函式回撥也是在每隔1000毫秒之後發生的,這是由Timer物件的建構函式中的第三個引數所決定的。程式會在1000毫秒的時間間隔後不斷的產生新執行緒,只到使用者輸入回車才結束執行。不過值得注意的是,雖然我們設定了時間間隔為1000毫秒,但是實際執行的時候往往並不能非常精確。因為Windows作業系統並不是一個實時系統,而公用語言執行時也不是實時的,所以由於執行緒排程的千變萬化,實際的執行效果往往是不能精確到毫秒級的,但是對於一般的應用來說那已經是足夠的了,所以你也不必十分苛求。



小結



本文介紹了在.Net下進行多執行緒程式設計所需要掌握的一些基本知識。從文章中我們可以知道在.Net下進行多執行緒程式設計相對以前是有了大大的簡化,但是其功能並沒有被削弱。使用以上的一些基本知識,讀者就可以試著編寫.Net下的多執行緒程式了。不過要編寫出功能更加強大而且Bug少的多執行緒應用程式,讀者需要掌握諸如執行緒同步、執行緒池等高階的多執行緒程式設計技術。讀者不妨參考一些作業系統方面或是多執行緒程式設計方面的技術叢書。 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-557465/,如需轉載,請註明出處,否則將追究法律責任。

相關文章