C#的多線程機(jī)制初探
2024-07-21 02:19:01
供稿:網(wǎng)友
注冊會員,創(chuàng)建你的web開發(fā)資料庫,注:本文中出現(xiàn)的代碼均在.net framework rc3環(huán)境中運行通過
一.多線程的概念
windows是一個多任務(wù)的系統(tǒng),如果你使用的是windows 2000及其以上版本,你可以通過任務(wù)管理器查看當(dāng)前系統(tǒng)運行的程序和進(jìn)程。什么是進(jìn)程呢?當(dāng)一個程序開始運行時,它就是一個進(jìn)程,進(jìn)程所指包括運行中的程序和程序所使用到的內(nèi)存和系統(tǒng)資源。而一個進(jìn)程又是由多個線程所組成的,線程是程序中的一個執(zhí)行流,每個線程都有自己的專有寄存器(棧指針、程序計數(shù)器等),但代碼區(qū)是共享的,即不同的線程可以執(zhí)行同樣的函數(shù)。多線程是指程序中包含多個執(zhí)行流,即在一個程序中可以同時運行多個不同的線程來執(zhí)行不同的任務(wù),也就是說允許單個程序創(chuàng)建多個并行執(zhí)行的線程來完成各自的任務(wù)。瀏覽器就是一個很好的多線程的例子,在瀏覽器中你可以在下載java小應(yīng)用程序或圖象的同時滾動頁面,在訪問新頁面時,播放動畫和聲音,打印文件等。
多線程的好處在于可以提高cpu的利用率——任何一個程序員都不希望自己的程序很多時候沒事可干,在多線程程序中,一個線程必須等待的時候,cpu可以運行其它的線程而不是等待,這樣就大大提高了程序的效率。
然而我們也必須認(rèn)識到線程本身可能影響系統(tǒng)性能的不利方面,以正確使用線程:
線程也是程序,所以線程需要占用內(nèi)存,線程越多占用內(nèi)存也越多
多線程需要協(xié)調(diào)和管理,所以需要cpu時間跟蹤線程
線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題
線程太多會導(dǎo)致控制太復(fù)雜,最終可能造成很多bug
基于以上認(rèn)識,我們可以一個比喻來加深理解。假設(shè)有一個公司,公司里有很多各司其職的職員,那么我們可以認(rèn)為這個正常運作的公司就是一個進(jìn)程,而公司里的職員就是線程。一個公司至少得有一個職員吧,同理,一個進(jìn)程至少包含一個線程。在公司里,你可以一個職員干所有的事,但是效率很顯然是高不起來的,一個人的公司也不可能做大;一個程序中也可以只用一個線程去做事,事實上,一些過時的語言如fortune,basic都是如此,但是象一個人的公司一樣,效率很低,如果做大程序,效率更低——事實上現(xiàn)在幾乎沒有單線程的商業(yè)軟件。公司的職員越多,老板就得發(fā)越多的薪水給他們,還得耗費大量精力去管理他們,協(xié)調(diào)他們之間的矛盾和利益;程序也是如此,線程越多耗費的資源也越多,需要cpu時間去跟蹤線程,還得解決諸如死鎖,同步等問題。總之,如果你不想你的公司被稱為“皮包公司”,你就得多幾個員工;如果你不想讓你的程序顯得稚氣,就在你的程序里引入多線程吧!
本文將對c#編程中的多線程機(jī)制進(jìn)行探討,通過一些實例解決對線程的控制,多線程間通訊等問題。為了省去創(chuàng)建gui那些繁瑣的步驟,更清晰地逼近線程的本質(zhì),下面所有的程序都是控制臺程序,程序最后的console.readline()是為了使程序中途停下來,以便看清楚執(zhí)行過程中的輸出。
好了,廢話少說,讓我們來體驗一下多線程的c#吧!
二.操縱一個線程
任何程序在執(zhí)行時,至少有一個主線程,下面這段小程序可以給讀者一個直觀的印象:
[code]
//systemthread.cs
using system;
using system.threading;
namespace threadtest
{
class runit
{
[stathread]
static void main(string[] args)
{
thread.currentthread.name="system thread";//給當(dāng)前線程起名為"system thread"
console.writeline(thread.currentthread.name+"'status:"+thread.currentthread.threadstate);
console.readline();
}
}
}
[/code]
編譯執(zhí)行后你看到了什么?是的,程序?qū)a(chǎn)生如下輸出:
system thread's status:running
在這里,我們通過thread類的靜態(tài)屬性currentthread獲取了當(dāng)前執(zhí)行的線程,對其name屬性賦值“system thread”,最后還輸出了它的當(dāng)前狀態(tài)(threadstate)。所謂靜態(tài)屬性,就是這個類所有對象所公有的屬性,不管你創(chuàng)建了多少個這個類的實例,但是類的靜態(tài)屬性在內(nèi)存中只有一個。很容易理解currentthread為什么是靜態(tài)的——雖然有多個線程同時存在,但是在某一個時刻,cpu只能執(zhí)行其中一個。
就像上面程序所演示的,我們通過thread類來創(chuàng)建和控制線程。注意到程序的頭部,我們使用了如下命名空間:
以下內(nèi)容為程序代碼:
using system;
using system.threading;
在.net framework class library中,所有與多線程機(jī)制應(yīng)用相關(guān)的類都是放在system.threading命名空間中的。其中提供thread類用于創(chuàng)建線程,threadpool類用于管理線程池等等,此外還提供解決了線程執(zhí)行安排,死鎖,線程間通訊等實際問題的機(jī)制。如果你想在你的應(yīng)用程序中使用多線程,就必須包含這個類。thread類有幾個至關(guān)重要的方法,描述如下:
start():啟動線程
sleep(int):靜態(tài)方法,暫停當(dāng)前線程指定的毫秒數(shù)
abort():通常使用該方法來終止一個線程
suspend():該方法并不終止未完成的線程,它僅僅掛起線程,以后還可恢復(fù)。
resume():恢復(fù)被suspend()方法掛起的線程的執(zhí)行
下面我們就動手來創(chuàng)建一個線程,使用thread類創(chuàng)建線程時,只需提供線程入口即可。線程入口使程序知道該讓這個線程干什么事,在c#中,線程入口是通過threadstart代理(delegate)來提供的,你可以把threadstart理解為一個函數(shù)指針,指向線程要執(zhí)行的函數(shù),當(dāng)調(diào)用thread.start()方法后,線程就開始執(zhí)行threadstart所代表或者說指向的函數(shù)。
打開你的vs.net,新建一個控制臺應(yīng)用程序(console application),下面這些代碼將讓你體味到完全控制一個線程的無窮樂趣!
//threadtest.cs
using system;
using system.threading;
namespace threadtest
{
public class alpha
{
public void beta()
{
while (true)
{
console.writeline("alpha.beta is running in its own thread.");
}
}
};
public class simple
{
public static int main()
{
console.writeline("thread start/stop/join sample");
alpha oalpha = new alpha();
//這里創(chuàng)建一個線程,使之執(zhí)行alpha類的beta()方法
thread othread = new thread(new threadstart(oalpha.beta));
othread.start();
while (!othread.isalive);
thread.sleep(1);
othread.abort();
othread.join();
console.writeline();
console.writeline("alpha.beta has finished");
try
{
console.writeline("try to restart the alpha.beta thread");
othread.start();
}
catch (threadstateexception)
{
console.write("threadstateexception trying to restart alpha.beta. ");
console.writeline("expected since aborted threads cannot be restarted.");
console.readline();
}
return 0;
}
}
}
這段程序包含兩個類alpha和simple,在創(chuàng)建線程othread時我們用指向alpha.beta()方法的初始化了threadstart代理(delegate)對象,當(dāng)我們創(chuàng)建的線程othread調(diào)用othread.start()方法啟動時,實際上程序運行的是alpha.beta()方法:
alpha oalpha = new alpha();
thread othread = new thread(new threadstart(oalpha.beta));
othread.start();
然后在main()函數(shù)的while循環(huán)中,我們使用靜態(tài)方法thread.sleep()讓主線程停了1ms,這段時間cpu轉(zhuǎn)向執(zhí)行線程othread。然后我們試圖用thread.abort()方法終止線程othread,注意后面的othread.join(),thread.join()方法使主線程等待,直到othread線程結(jié)束。你可以給thread.join()方法指定一個int型的參數(shù)作為等待的最長時間。之后,我們試圖用thread.start()方法重新啟動線程othread,但是顯然abort()方法帶來的后果是不可恢復(fù)的終止線程,所以最后程序會拋出threadstateexception異常。
在這里我們要注意的是其它線程都是依附于main()函數(shù)所在的線程的,main()函數(shù)是c#程序的入口,起始線程可以稱之為主線程,如果所有的前臺線程都停止了,那么主線程可以終止,而所有的后臺線程都將無條件終止。而所有的線程雖然在微觀上是串行執(zhí)行的,但是在宏觀上你完全可以認(rèn)為它們在并行執(zhí)行。
讀者一定注意到了thread.threadstate這個屬性,這個屬性代表了線程運行時狀態(tài),在不同的情況下有不同的值,于是我們有時候可以通過對該值的判斷來設(shè)計程序流程。threadstate在各種情況下的可能取值如下:
aborted:線程已停止
abortrequested:線程的thread.abort()方法已被調(diào)用,但是線程還未停止
background:線程在后臺執(zhí)行,與屬性thread.isbackground有關(guān)
running:線程正在正常運行
stopped:線程已經(jīng)被停止
stoprequested:線程正在被要求停止
suspended:線程已經(jīng)被掛起(此狀態(tài)下,可以通過調(diào)用resume()方法重新運行)
suspendrequested:線程正在要求被掛起,但是未來得及響應(yīng)
unstarted:未調(diào)用thread.start()開始線程的運行
waitsleepjoin:線程因為調(diào)用了wait(),sleep()或join()等方法處于封鎖狀態(tài)
上面提到了background狀態(tài)表示該線程在后臺運行,那么后臺運行的線程有什么特別的地方呢?其實后臺線程跟前臺線程只有一個區(qū)別,那就是后臺線程不妨礙程序的終止。一旦一個進(jìn)程所有的前臺線程都終止后,clr(通用語言運行環(huán)境)將通過調(diào)用任意一個存活中的后臺進(jìn)程的abort()方法來徹底終止進(jìn)程。
當(dāng)線程之間爭奪cpu時間時,cpu按照是線程的優(yōu)先級給予服務(wù)的。在c#應(yīng)用程序中,用戶可以設(shè)定5個不同的優(yōu)先級,由高到低分別是highest,abovenormal,normal,belownormal,lowest,在創(chuàng)建線程時如果不指定優(yōu)先級,那么系統(tǒng)默認(rèn)為threadpriority.normal。給一個線程指定優(yōu)先級
,我們可以使用如下代碼:
//設(shè)定優(yōu)先級為最低
mythread.priority=threadpriority.lowest;
通過設(shè)定線程的優(yōu)先級,我們可以安排一些相對重要的線程優(yōu)先執(zhí)行,例如對用戶的響應(yīng)等等。
現(xiàn)在我們對怎樣創(chuàng)建和控制一個線程已經(jīng)有了一個初步的了解,下面我們將深入研究線程實現(xiàn)中比較典型的的問題,并且探討其解決方法。
三.線程的同步和通訊——生產(chǎn)者和消費者
假設(shè)這樣一種情況,兩個線程同時維護(hù)一個隊列,如果一個線程對隊列中添加元素,而另外一個線程從隊列中取用元素,那么我們稱添加元素的線程為生產(chǎn)者,稱取用元素的線程為消費者。生產(chǎn)者與消費者問題看起來很簡單,但是卻是多線程應(yīng)用中一個必須解決的問題,它涉及到線程之間的同步和通訊問題。
前面說過,每個線程都有自己的資源,但是代碼區(qū)是共享的,即每個線程都可以執(zhí)行相同的函數(shù)。但是多線程環(huán)境下,可能帶來的問題就是幾個線程同時執(zhí)行一個函數(shù),導(dǎo)致數(shù)據(jù)的混亂,產(chǎn)生不可預(yù)料的結(jié)果,因此我們必須避免這種情況的發(fā)生。c#提供了一個關(guān)鍵字lock,它可以把一段代碼定義為互斥段(critical section),互斥段在一個時刻內(nèi)只允許一個線程進(jìn)入執(zhí)行,而其他線程必須等待。在c#中,關(guān)鍵字lock定義如下:
lock(expression) statement_block
expression代表你希望跟蹤的對象,通常是對象引用。一般地,如果你想保護(hù)一個類的實例,你可以使用this;如果你希望保護(hù)一個靜態(tài)變量(如互斥代碼段在一個靜態(tài)方法內(nèi)部),一般使用類名就可以了。而statement_block就是互斥段的代碼,這段代碼在一個時刻內(nèi)只可能被一個線程執(zhí)行。
下面是一個使用lock關(guān)鍵字的典型例子,我將在注釋里向大家說明lock關(guān)鍵字的用法和用途:
//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(int amount)
{
if (balance < 0)
{
//如果balance小于0則拋出異常
throw new exception("negative balance");
}
//下面的代碼保證在當(dāng)前線程修改balance的值完成之前
//不會有其他線程也執(zhí)行這段代碼來修改balance的值
//因此,balance的值是不可能小于0的
lock (this)
{
console.writeline("current thread:"+thread.currentthread.name);
//如果沒有l(wèi)ock關(guān)鍵字的保護(hù),那么可能在執(zhí)行完if的條件判斷之后
//另外一個線程卻執(zhí)行了balance=balance-amount修改了balance的值
//而這個修改對這個線程是不可見的,所以可能導(dǎo)致這時if的條件已經(jīng)不成立了
//但是,這個線程卻繼續(xù)執(zhí)行balance=balance-amount,所以導(dǎo)致balance可能小于0
if (balance >= amount)
{
thread.sleep(5);
balance = balance - amount;
return amount;
}
else
{
return 0; // transaction rejected
}
}
}
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(new threadstart(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();
}
}
而多線程公用一個對象時,也會出現(xiàn)和公用代碼類似的問題,這種問題就不應(yīng)該使用lock關(guān)鍵字了,這里需要用到system.threading中的一個類monitor,我們可以稱之為監(jiān)視器,monitor提供了使線程共享資源的方案。
monitor類可以鎖定一個對象,一個線程只有得到這把鎖才可以對該對象進(jìn)行操作。對象鎖機(jī)制保證了在可能引起混亂的情況下一個時刻只有一個線程可以訪問這個對象。monitor必須和一個具體的對象相關(guān)聯(lián),但是由于它是一個靜態(tài)的類,所以不能使用它來定義對象,而且它的所有方法都是靜態(tài)的,不能使用對象來引用。下面代碼說明了使用monitor鎖定一個對象的情形:
......
queue oqueue=new queue();
......
monitor.enter(oqueue);
......//現(xiàn)在oqueue對象只能被當(dāng)前線程操縱了
monitor.exit(oqueue);//釋放鎖
如上所示,當(dāng)一個線程調(diào)用monitor.enter()方法鎖定一個對象時,這個對象就歸它所有了,其它線程想要訪問這個對象,只有等待它使用monitor.exit()方法釋放鎖。為了保證線程最終都能釋放鎖,你可以把monitor.exit()方法寫在try-catch-finally結(jié)構(gòu)中的finally代碼塊里。對于任何一個被monitor鎖定的對象,內(nèi)存中都保存著與它相關(guān)的一些信息,其一是現(xiàn)在持有鎖的線程的引用,其二是一個預(yù)備隊列,隊列中保存了已經(jīng)準(zhǔn)備好獲取鎖的線程,其三是一個等待隊列,隊列中保存著當(dāng)前正在等待這個對象狀態(tài)改變的隊列的引用。當(dāng)擁有對象鎖的線程準(zhǔn)備釋放鎖時,它使用monitor.pulse()方法通知等待隊列中的第一個線程,于是該線程被轉(zhuǎn)移到預(yù)備隊列中,當(dāng)對象鎖被釋放時,在預(yù)備隊列中的線程可以立即獲得對象鎖。
下面是一個展示如何使用lock關(guān)鍵字和monitor類來實現(xiàn)線程的同步和通訊的例子,也是一個典型的生產(chǎn)者與消費者問題。這個例程中,生產(chǎn)者線程和消費者線程是交替進(jìn)行的,生產(chǎn)者寫入一個數(shù),消費者立即讀取并且顯示,我將在注釋中介紹該程序的精要所在。用到的系統(tǒng)命名空間如下:
using system;
using system.threading;