天天看點

C#多線程程式設計介紹——使用thread、threadpool、timer

在system.threading 命名空間提供一些使得能進行多線程程式設計的類和接口,其中線程的建立有以下三種方法:thread、threadpool、timer。下面我就他們的使用方法逐個作一簡單介紹。 

1. 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”   

console.write("2");   

public static void dosth()   

{//用來模拟複雜運算   

for (int j = 0; j < 10000000; j++)   

int a=15;   

aa = a*a*a*a;   

以上程式運作結果為:

11111111111111111111111111111111111111111121111111111111111111111111111111111111111112

從以上結果我們能看出,t1線程所占用cpu的時間遠比t2的多,這是因為t1的優先級比t2的高,若我們把t1和t2的優先級都設為normal,那結果是怎麼?他們所占用的cpu時間會相同嗎?是的,正如你所料,見下圖:

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的調用方法):

t1.interrupt ();   

e.waitone ();   

t1.join();   

console.writeline(“t1 is end”);   

static autoresetevent e = new autoresetevent(false);   

try   

{//從參數可看出将導緻休眠   

thread.sleep(timeout.infinite);   

catch(system.threading.threadinterruptedexception e)   

{//中斷處理程式   

console.writeline (" 1st interrupt");   

e.set ();   

{// 休眠   

thread.sleep(timeout.infinite );   

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,如上例)。 

2. threadpool 

提供一個線程池,該線程池可用于發送工作項、處理異步 I/O、代表其他線程等待以及處理計時器。

線程池(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),   

,   

false//表示每次完成等待操作後都重置計時器,直到登出等待   

threadpool.queueuserworkitem (new waitcallback (threadfunc),8);   

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 true

從以上結果我們能看出線程threadfunc運作了1次,而waitthreadfunc運作了5次。我們能從waitortimercallback中的bool t參數判斷啟動此線程的原因:t為false,則表示由于waitobject,否則則是由于逾時。另外我們也能通過object b向線程傳遞一些參數。 

3. timer 

提供以指定的時間間隔執行方法的機制。

使用 TimerCallback 委托指定希望 Timer 執行的方法。 計時器委托在構造計時器時指定,并且不能更改。 此方法不在建立計時器的線程上執行,而是在系統提供的 ThreadPool 線程上執行。

建立計時器時,可以指定在第一次執行方法之前等待的時間量(截止時間)以及此後的執行期間等待的時間量(時間周期)。 可以使用 Change 方法更改這些值或禁用計時器。 

隻要在使用 Timer,就必須保留對它的引用。 對于任何托管對象,如果沒有對 Timer 的引用,計時器會被垃圾回收。 即使 Timer 仍處在活動狀态,也會被回收。 

當不再需要計時器時,請使用 Dispose 方法釋放計時器持有的資源。 如果希望在計時器被釋放時接收到信号,請使用接受 WaitHandle 的 Dispose(WaitHandle) 方法重載。 計時器已被釋放後,WaitHandle 便終止。

由計時器執行的回調方法應該是可重入的,因為它是在 ThreadPool 線程上調用的。 在以下兩種情況中,此回調可以同時在兩個線程池線程上執行:一是計時器間隔小于執行此回調所需的時間;二是所有線程池線程都在使用,此回調被多次排隊。

System.Threading.Timer 是一個簡單的輕量計時器,它使用回調方法并由線程池線程提供服務。 不建議将其用于 Windows 窗體,因為其回調不在使用者界面線程上進行。 System.Windows.Forms.Timer 是用于 Windows 窗體的更佳選擇。 要擷取基于伺服器的計時器功能,可以考慮使用 System.Timers.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方法來改動: 

[c#] 

public bool change( 

int duetime, 

int period 

);//顯然所改動的兩個參數對應于timer中的兩參數 

見下例:

{ 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);   

public static void timercall(object b)   

console.writeline ("timercallback; b is {0}",b);   

period is 1000

timercallback;b is 3

period is 500

timercallback;b is 3 

總結 

從以上的簡單介紹,我們能看出他們各自使用的場合:thread适用于那些需對線程進行複雜控制的場合;threadpool适應于一些需要多個線程而又較短任務(如一些常處于阻塞狀态的線程);timer則适用于那些需周期性調用的方法。隻要我們了解了他們的使用特點,我們就能非常好的選擇合适的方法。

本文轉自linzheng 51CTO部落格,原文連結:http://blog.51cto.com/linzheng/1085602