NET將關於多執行緒的功能定義在System.Threading名字空間中。因此,要使用多線程,必須先聲明引用此名字空間(using System.Threading;)。
a.啟動執行緒顧名思義,「啟動執行緒」就是新建並啟動一個執行緒的意思,如下程式碼可實現:
Thread thread1 = new Thread(new ThreadStart( Count));
其中的Count 是將要被新執行緒執行的函數。
b.殺死線程
「殺死線程」就是將一線程斬草除根,為了不白費力氣,在殺死一個線程前最好先判斷它是否還活著(通過IsAlive 屬性),然後就可以調用Abort 方法來殺死此線程。
c.暫停線程它的意思就是讓一個正在運行的線程休眠一段時間。如thread.Sleep(1000); 就是讓執行緒休眠1秒鐘。
d.優先順序這個用不著解釋了。 Thread類別中hreadPriority屬性,它用來設定優先級,但不能保證作業系統會接受該優先級。一個執行緒的優先權可分為5種:Normal, AboveNormal, BelowNormal, Highest, Lowest。具體實現例子如下:
thread.Priority = ThreadPriority.Highest;
e.掛起線程
Thread類別的Suspend方法用來掛起線程,直到呼叫Resume,此線程才可以繼續執行。如果線程已經掛起,那就不會起作用。
if (thread.ThreadState = ThreadState.Running)
{
thread.Suspend();
}
f.恢復線程用來恢復已經掛起的線程,讓它繼續執行,如果線程沒有掛起,就不會起作用。
if (thread.ThreadState = ThreadState.Suspended)
{
thread.Resume();
}
下面將列出一個例子,以說明簡單的執行緒處理功能。此範例來自於幫助文件。
using System;
using System.Threading;
// Simple threading scenario: Start a static method running
// on a second thread.
public class ThreadExample {
// The ThreadProc method is called when the thread starts.
// It loops ten times, writing to the console and yielding
// the rest of its time slice each time, and then ends.
public static void ThreadProc() {
for (int i = 0; i < 10; i++) {
Console.WriteLine("ThreadProc: {0}", i);
// Yield the rest of the time slice.
Thread.Sleep(0);
}
}
public static void Main() {
Console.WriteLine("Main thread: Start a second thread.");
// The constructor for the Thread class requires a ThreadStart
// delegate that represents the method to be executed on the
// thread. C# simplifies the creation of this delegate.
Thread t = new Thread(new ThreadStart(ThreadProc));
// Start ThreadProc. On a uniprocessor, the thread does not get
// any processor time until the main thread yields. Uncomment
// the Thread.Sleep that follows t.Start() to see the difference.
t.Start();
//Thread.Sleep(0);
for (int i = 0; i < 4; i++) {
Console.WriteLine("Main thread: Do some work.");
Thread.Sleep(0);
}
Console.WriteLine("Main thread: Call Join(), to wait until ThreadProc ends.");
t.Join();
Console.WriteLine("Main thread: ThreadProc.Join has returned. Press Enter to end program.");
Console.ReadLine();
}
}
此程式碼產生的輸出類似如下內容:
Main thread: Start a second thread.
Main thread: Do some work.
ThreadProc: 0
Main thread: Do some work.
ThreadProc: 1
Main thread: Do some work.
ThreadProc: 2
Main thread: Do some work.
ThreadProc: 3
Main thread: Call Join(), to wait until ThreadProc ends.
ThreadProc: 4
ThreadProc: 5
ThreadProc: 6
ThreadProc: 7
ThreadProc: 8
ThreadProc: 9
Main thread: ThreadProc.Join has returned. Press Enter to end program.
在Visul C#中System.Threading 命名空間提供一些使得可以進行多線程編程的類別和接口,其中線程的創建有以下三種方法:Thread、ThreadPool、Timer。以下將它們的使用方法逐一作一簡單介紹。
一、Thread
這也許是最複雜的方法,但它提供了對線程的各種靈活控制。首先你必須使用它的建構子來建立一個執行緒實例,它的參數比較簡單,只有一個ThreadStart 委託:public Thread(ThreadStart start);然後呼叫Start()啟動它,當然你可以利用它的Priority屬性來設定或取得它的運作優先權(enum ThreadPriority: Normal、 Lowest、 Highest、 BelowNormal、 AboveNormal)。
下例首先產生了兩個執行緒實例t1和t2,然後分別設定它們的優先權,接著啟動兩執行緒(兩執行緒基本上一樣,只不過它們輸出不一樣,t1為“1”,t2為“2”,根據它們各自輸出字元個數比可大致看出它們佔用CPU時間之比,這也反映出了它們各自的優先級)。
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(Thread1));
Thread t2 = new Thread(new ThreadStart(Thread2));
t1.Priority = ThreadPriority.BelowNormal ;
t2.Priority = ThreadPriority.Lowest ;
t1.Start();
t2.Start();
}
public static void Thread1()
{
for (int i = 1; i < 1000; i++)
{//每運行一個循環就寫一個“1”
dosth();
Console.Write("1");
}
}
public static void Thread2()
{
for (int i = 0; i < 1000; i++)
{//每運行一個循環就寫一個“2”
dosth();
Console.Write("2");
}
}
public static void dosth()
{//用來模擬複雜運算
for (int j = 0; j < 10000000; j++)
{
int a=15;
a = a*a*a*a;
}
}
以上程序運行結果為:
1111111111111111111111111111111111111111121111111111111111111111111111111111111111112
1111111111111111111111111111111111111111121111111111111111111111111111111111111111112
1111111111111111111111111111111111111111121111111111111111111111111111111111111111112
從上述結果我們可以看出,t1執行緒所佔用CPU的時間遠比t2的多,這是因為t1的優先權比t2的高,若我們把t1和t2的優先權都設為Normal,結果見下圖:
121211221212121212121212121212121212121212121212121212121212121212121
212121212121212121212121212121212121212121212121212121212121212121212
121212121212121212
從上例我們可以看出,它的建構類似win32的工作線程,但更簡單,只要把線程要呼叫的函式當作委託,然後把委託當作參數建構線程實例即可。當呼叫Start()啟動後,便會呼叫對應的函數,從那函數第一行開始執行。
接下來我們結合線程的ThreadState屬性來了解線程的控制。 ThreadState是一個枚舉類型,它反映的是執行緒所處的狀態。當一個Thread實例剛創建時,它的ThreadState是Unstarted;當此線程被調用Start()啟動之後,它的ThreadState是Running; 在此線程啟動之後,如果想讓它暫停(阻塞),可以調用Thread. Sleep() 方法,它有兩個重載方法(Sleep(int )、Sleep(Timespan )),只不過是表示時間量的格式不同而已,當在某線程內調用此函數時,它表示此線程將阻塞一段時間(時間是由傳遞給Sleep 的毫秒數或Timespan決定的,但若參數為0則表示掛起此線程以使其它線程能夠執行,指定Infinite 以無限期阻塞線程),此時它的ThreadState會變成WaitSleepJoin,另外值得注意一點的是Sleep()函數定義為了static? ! 這也意味著它不能和某個執行緒實例結合起來用,也即不存在類似t1.Sleep(10)的呼叫!正是如此,Sleep()函數只能由需「Sleep」的線程自己調用,不允許其它線程調用,正如when to Sleep是個人私事不能由它人決定。但當某執行緒處於WaitSleepJoin狀態而又不得不喚醒它時,可使用Thread.Interrupt 方法,它將在執行緒上引發ThreadInterruptedException,下面我們先看一個範例(注意Sleep的呼叫方法):
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(Thread1));
t1.Start();
t1.Interrupt ();
E.WaitOne ();
t1.Interrupt ();
t1.Join();
Console.WriteLine(“t1 is end”);
}
static AutoResetEvent E = new AutoResetEvent(false);
public static void Thread1()
{
try
{//從參數可看出會導致休眠
Thread.Sleep(Timeout.Infinite);
}
catch(System.Threading.ThreadInterruptedException e)
{//中斷處理程序
Console.WriteLine (" 1st interrupt");
}
E.Set ();
try
{// 休眠
Thread.Sleep(Timeout.Infinite );
}
catch(System.Threading.ThreadInterruptedException e)
{
Console.WriteLine (" 2nd interrupt");
}//暫停10秒
Thread.Sleep (10000);
}
運行結果為:1st interrupt
2nd interrupt
(10s後)t1 is end
從上例我們可以看出Thread.Interrupt方法可以把程序從某個阻塞(WaitSleepJoin)狀態喚醒進入對應的中斷處理程序,然後繼續往下執行(它的ThreadState也變為Running),此函數的使用必須注意以下幾點:
1.此方法不僅可喚醒由Sleep導致的阻塞,而且對一切可導致執行緒進入WaitSleepJoin狀態的方法(如Wait和Join)都有效。如上例所示, 使用時要把導致執行緒阻塞的方法放入try區塊內, 並把對應的中斷處理程序放入catch區塊內。
2、對某一執行緒呼叫Interrupt, 如它正處於WaitSleepJoin狀態, 則進入對應的中斷處理程序執行, 若此時它不處於WaitSleepJoin狀態, 則它後來進入此狀態時, 將立即中斷。若中斷前調用幾次Interrupt, 只有第一次調用有效, 這正是上例我用同步的原因, 這樣才能確保第二次調用Interrupt在第一個中斷後調用,否則的話可能導致第二次呼叫無效(若它在第一個中斷前呼叫)。你可以把同步去掉試試,結果很可能是: 1st interrupt
上例也用了另外兩個讓執行緒進入WaitSleepJoin狀態的方法:利用同步物件和Thread.Join方法。 Join方法的使用比較簡單,它表示在呼叫此方法的當前執行緒阻塞直至另一執行緒(此例中是t1)終止或經過了指定的時間為止(若它還帶了時間量參數),當兩個條件(若有)任一出現,它立即結束WaitSleepJoin狀態進入Running狀態(可根據.Join方法的返回值判斷為何種條件,為true,則是線程終止;false則是時間到)。執行緒的暫停也可用Thread.Suspend方法,當某執行緒處於Running狀態時對它呼叫Suspend方法,它將進入SuspendRequested狀態,但它並不會被立即掛起,直到執行緒到達安全點之後它才可以將該執行緒掛起,此時它將進入Suspended狀態。如對一個已處於Suspended的執行緒呼叫則無效,要恢復執行只需呼叫Thread.Resume即可。
最後我們談的是執行緒的銷毀,我們可以對需要銷毀的執行緒呼叫Abort方法,它會在此執行緒上引發ThreadAbortException。我們可把線程內的一些程式碼放入try塊內,並把對應處理程式碼放入對應的catch塊內,當線程正執行try塊內程式碼時如被調用Abort,它便會跳入對應的catch塊內執行,執行完catch快內的程式碼後它將終止(若catch塊內執行了ResetAbort則不同了:它將取消當前Abort請求,繼續向下執行。所以如要確保某線程終止的最好用Join ,如上例)。
二、ThreadPool
線程池(ThreadPool)是一種相對較簡單的方法,它適應於一些需要多個線程而又較短任務(如一些常處於阻塞狀態的線程) ,它的缺點是對創建的線程不能加以控制,也不能設定其優先權。由於每個行程只有一個執行緒池,當然每個應用程式域也只有一個執行緒池(對線),所以你將發現ThreadPool類別的成員函數都為static! 當你第一次呼叫ThreadPool.QueueUserWorkItem、ThreadPool.RegisterWaitForSingleObject等,便會建立執行緒池實例。下面就線程池當中的兩函數作一介紹:
public static bool QueueUserWorkItem( //呼叫成功則回傳true
WaitCallback callBack,//要建立的執行緒呼叫的委託
object state //傳遞給委託的參數
)//它的另一個重載函數類似,只是委託不帶參數而已此函數的作用是把要創建的線程排隊到線程池,當線程池的可用線程數不為零時(線程池有創建線程數的限制,缺身值為25),便建立此線程,否則就排隊到線程池等到它有可用的線程時才創建。
public static RegisteredWaitHandle RegisterWaitForSingleObject(
WaitHandle waitObject,// 要註冊的WaitHandle
WaitOrTimerCallback callBack,// 執行緒呼叫的委託
object state,//傳遞給委託的參數
int TimeOut,//逾時,單位為毫秒,
bool executeOnlyOnce file://是否只執行一次
);
public delegate void WaitOrTimerCallback(
object state,//也即傳遞給委託的參數
bool timedOut//true表示由於逾時調用,反之則因為waitObject
);
此函數的作用是建立一個等待線程,一旦呼叫此函數便建立此線程,在參數waitObject變成終止狀態或設定的時間TimeOut到了之前,它都處於「阻塞」狀態,值得注意的一點是此「阻塞」與Thread的WaitSleepJoin狀態有很大的不同:當某Thread處於WaitSleepJoin狀態時CPU會定期的喚醒它以輪詢更新狀態信息,然後再次進入WaitSleepJoin狀態,線程的切換可是很費資源的;而用此函數創建的線程則不同,在觸發它運行之前,CPU不會切換到此線程,它既不佔用CPU的時間又不浪費線程切換時間,但CPU又如何知道何時運行它?實際上線程池會產生一些輔助線程用來監視這些觸發條件,一旦達到條件便啟動相應的線程,當然這些輔助線程本身也佔用時間,但是如果你需創建較多的等待線程時,使用線程池的優勢就越加明顯。見下例:
static AutoResetEvent ev=new AutoResetEvent(false);
public static int Main(string[] args)
{ ThreadPool.RegisterWaitForSingleObject(
ev,
new WaitOrTimerCallback(WaitThreadFunc),
4,
2000,
false//表示每次完成等待操作後都重置計時器,直到登出等待
);
ThreadPool.QueueUserWorkItem (new WaitCallback (ThreadFunc),8);
Thread.Sleep (10000);
return 0;
}
public static void ThreadFunc(object b)
{ Console.WriteLine ("the object is {0}",b);
for(int i=0;i<2;i++)
{ Thread.Sleep (1000);
ev.Set();
}
}
public static void WaitThreadFunc(object b,bool t)
{ Console.WriteLine ("the object is {0},t is {1}",b,t);
}
其運行結果為:
the object is 8
the object is 4,t is False
the object is 4,t is False
the object is 4,t is True
the object is 4,t is True
the object is 4,t is True
從上述結果我們可以看出執行緒ThreadFunc運行了1次,而WaitThreadFunc運行了5次。我們可以從WaitOrTimerCallback中的bool t參數判斷啟動此執行緒的原因:t為false,則表示由於waitObject,否則則是因為逾時。另外我們也可以透過object b向線程傳遞一些參數。
3、Timer
它適用於需週期性呼叫的方法,它不在創建計時器的執行緒中運行,它在由系統自動分配的單獨執行緒中運行。這和Win32中的SetTimer方法類似。它的構造為:
public Timer(
TimerCallback callback,//所需呼叫的方法
object state,//傳遞給callback的參數
int dueTime,//多久後開始呼叫callback
int period//呼叫此方法的時間間隔
); // 如果dueTime 為0,則callback 立即執行它的首次呼叫。如果dueTime 為Infinite,則callback 不呼叫它的方法。計時器已停用,但使用Change 方法可以重新啟用它。如果period 為0或Infinite,且dueTime 不為Infinite,則callback 呼叫它的方法一次。計時器的定期行為被停用,但使用Change 方法可以重新啟用它。如果period 為零(0) 或Infinite,且dueTime 不為Infinite,則callback 呼叫它的方法一次。計時器的定期行為被停用,但使用Change 方法可以重新啟用它。
在創建計時器之後若想改變它的period和dueTime,我們可以透過呼叫Timer的Change方法來改變:
public bool Change(
int dueTime,
int period
);//顯然所改變的兩個參數對應於Timer中的兩個參數
public static int Main(string[] args)
{
Console.WriteLine ("period is 1000");
Timer tm=new Timer (new TimerCallback (TimerCall),3,1000,1000);
Thread.Sleep (2000);
Console.WriteLine ("period is 500");
tm.Change (0,800);
Thread.Sleep (3000);
return 0;
}
public static void TimerCall(object b)
{
Console.WriteLine ("timercallback; b is {0}",b);
}
其運行結果為:
period is 1000
timercallback;b is 3
timercallback;b is 3
period is 500
timercallback;b is 3
timercallback;b is 3
timercallback;b is 3
timercallback;b is 3
總結
從以上的簡單介紹,我們可以看出它們各自使用的場合:Thread適用於那些需對線程進行複雜控制的場合;ThreadPool適應於一些需要多個線程而又較短任務(如一些常處於阻塞狀態的線程);Timer則適用於那些需週期性呼叫的方法。只要我們了解了它們的使用特點,我們就可以很好的選擇合適的方法。