初識(shí)C#線程
發(fā)表時(shí)間:2024-06-18 來(lái)源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]作者: BUIILDER.COM使用多線程技術(shù)能有效地幫助你實(shí)現(xiàn)應(yīng)用程序的更高性能和更優(yōu)良的可伸縮性。但在真正運(yùn)用這項(xiàng)技術(shù)的時(shí)候務(wù)必小心。本文是對(duì)線程技術(shù)所牽扯的工具和技術(shù)問(wèn)題系列文章的開(kāi)篇。我首先對(duì)線程概念進(jìn)行介紹,然后總結(jié)一些常用的構(gòu)造,最后介紹它們的用法。線程的兩面性用Java語(yǔ)言編寫(xiě)多線程...
作者: BUIILDER.COM
使用多線程技術(shù)能有效地幫助你實(shí)現(xiàn)應(yīng)用程序的更高性能和更優(yōu)良的可伸縮性。但在真正運(yùn)用這項(xiàng)技術(shù)的時(shí)候務(wù)必小心。本文是對(duì)線程技術(shù)所牽扯的工具和技術(shù)問(wèn)題系列文章的開(kāi)篇。我首先對(duì)線程概念進(jìn)行介紹,然后總結(jié)一些常用的構(gòu)造,最后介紹它們的用法。
線程的兩面性
用Java語(yǔ)言編寫(xiě)多線程程序并不難,這是好事也是壞事。微軟在開(kāi)發(fā)C#時(shí),他們把這種易用性的窘境全盤(pán)照搬到了整個(gè)新平臺(tái)上。同時(shí),C#相比Java具有更多的程序原語(yǔ),但是Thread對(duì)象和同步監(jiān)視器的基本Java原語(yǔ)從形式和功能上看都已足夠提供強(qiáng)大的線程編程能力了。因此,在決定為應(yīng)用程序采用多線程技術(shù)之前務(wù)必小心。
為什么不用多線程
首先得記住,在決定是否采用多線程技術(shù)時(shí),除非你正在玩代碼,否則千萬(wàn)別因?yàn)槎嗑程編程夠“酷”而簡(jiǎn)單地使用線程技術(shù)編程。多線程編程技術(shù)太時(shí)髦了,如果你不小心點(diǎn)你的老板遲早也會(huì)著迷,那時(shí)你就死定了。其次,不要因?yàn)樽尦绦蜻\(yùn)行得更快而輕易采用多線程,除非你真的能證明單線程實(shí)現(xiàn)確實(shí)慢得可以。最后,在冒昧地一頭扎進(jìn)多線程機(jī)制之前,先回憶下微軟所提供的一種公寓(apartment)模型,也就是把對(duì)象寫(xiě)成單線程構(gòu)造而運(yùn)行在多線程環(huán)境下。所以,說(shuō)來(lái)說(shuō)去,你并不一定非要采用多線程編碼。不過(guò),公寓模型是另外一個(gè)話題了。
如果做得不對(duì),多線程編程勢(shì)必會(huì)打開(kāi)“潘朵拉的盒子”(意思是說(shuō)惹出無(wú)數(shù)的麻煩)。重復(fù)性不明顯、產(chǎn)生程序垃圾、記數(shù)器沒(méi)有正確增值等等。你的應(yīng)用程序還可能突然掛起。例如,數(shù)據(jù)庫(kù)連接這類資源就可能出人意料地關(guān)閉或者變得過(guò)載。高級(jí)開(kāi)發(fā)人員所面臨的一個(gè)大麻煩就是解決線程問(wèn)題。這些大問(wèn)題不花點(diǎn)時(shí)間休想解決,而且它們對(duì)產(chǎn)品交貨日期以及產(chǎn)品可靠性產(chǎn)生了嚴(yán)重的負(fù)面影響。
為什么要用多線程
如果你的應(yīng)用程序需要采取以下的操作,那么你盡可在編程的時(shí)候考慮多線程機(jī)制:
連續(xù)的操作,需要花費(fèi)忍無(wú)可忍的過(guò)長(zhǎng)時(shí)間才可能完成
并行計(jì)算
為了等待網(wǎng)絡(luò)、文件系統(tǒng)、用戶或其他I/O響應(yīng)而耗費(fèi)大量的執(zhí)行時(shí)間
所以說(shuō),在動(dòng)手之前,先保證自己的應(yīng)用程序中是否出現(xiàn)了以上3種情形。
如果你的代碼運(yùn)行得足夠快,但是你認(rèn)為你能讓它運(yùn)行得更快(假設(shè)你確實(shí)有這本事),我勸你最好不要接受這種誘惑。如果你不能肯定程序的計(jì)算操作并行性(例如針對(duì)同一數(shù)據(jù)表的并發(fā)數(shù)據(jù)庫(kù)更改——當(dāng)你的數(shù)據(jù)庫(kù)達(dá)到了數(shù)據(jù)表級(jí)鎖定的情況下),那么再想想其他法子吧。還有,如果你不知道應(yīng)用程序是否因?yàn)榈却斎牖蜉敵龆ㄙM(fèi)了過(guò)多的時(shí)間,那么請(qǐng)首先搞清楚真正耗費(fèi)時(shí)間的情況再說(shuō)。實(shí)際上,啟動(dòng)3個(gè)線程以百萬(wàn)分之一的步長(zhǎng)計(jì)算圓周率所消耗的時(shí)間就比同一線程重復(fù)計(jì)算3次要長(zhǎng)得多。為什么會(huì)出現(xiàn)這種失敗的情形呢?原因就在于,雖然第2條并行計(jì)算確實(shí)可用,但設(shè)計(jì)者卻恰恰忽略了以上第3個(gè)標(biāo)準(zhǔn):并行計(jì)算可以用到的一次計(jì)算期間卻沒(méi)有空閑周期。
假如你在為一臺(tái)裝備了多個(gè)處理器的并行計(jì)算機(jī)編寫(xiě)程序,則以上規(guī)則在這種情況下例外,你可以通過(guò)適當(dāng)?shù)牟⑿胁僮髟O(shè)計(jì)而令軟件性能大大獲益——哪怕每一操作都對(duì)CPU時(shí)間極其貪婪。
基本的線程管理工具
剛才我已經(jīng)為多線程編程提出了相當(dāng)程度的警告,同時(shí)還為何時(shí)使用或者不使用多線程提出了建議,接下來(lái)我對(duì)多線程編程所能利用的某些工具進(jìn)行闡述。
Thread對(duì)象
.NET庫(kù)提供了一種名為System.Threading.Thread的對(duì)象,這種對(duì)象代表了單一線程。你可以啟動(dòng)線程、在當(dāng)前線程繼續(xù)運(yùn)行的情況下設(shè)法完成線程的任務(wù)。這對(duì)那些需要打印文檔或者保存大型文件但希望獲得用戶確認(rèn)請(qǐng)求并給用戶返回控制的應(yīng)用程序來(lái)說(shuō)幫助實(shí)在太大了。我們通過(guò)程序清單A演示了這一機(jī)制。
程序清單A
using System;
using System.Threading;
namespace Threads1 {
class Listing1 {
static void SayHello() {
Console.WriteLine("Hello, ");
Thread.Sleep(750 /*mSec */);
Console.WriteLine("World");
}
static void Main(string[] args) {
Thread t1 = new Thread(new ThreadStartSayHello));
t1.Start();
Console.WriteLine("Thread started. Main done.");
}
}
}
我們首先創(chuàng)建了一種方法:SayHello,由它完成我們的任務(wù)——顯示問(wèn)候語(yǔ)。它的簽名必須匹配 System.Threading.ThreadStart指派(delegate)。注意,SayHello 方法調(diào)用了Thread.Sleep(int numMillisecs)方法。這是一種相當(dāng)有用的構(gòu)造而且會(huì)經(jīng)常出現(xiàn)在這類示例中。
在主程序中,我們通過(guò)帶SayHello方法的ThreadStart指派創(chuàng)建了一個(gè)新線程,并在該線程上調(diào)用Start方法。我們創(chuàng)建的線程隨之被啟動(dòng),然后我們的主線程在這個(gè)例子中繼續(xù)運(yùn)行到結(jié)束。
在很多情況下你可能要在各個(gè)線程中分別執(zhí)行存在輕微差別的任務(wù),同時(shí)需要把某種參數(shù)從一種任務(wù)所在的線程傳遞給另一任務(wù)所在的線程。要完成這一目標(biāo)可以采取好幾種合理的方式,最直接的做法就是創(chuàng)建一種Task對(duì)象,由它保存線程、特有的參數(shù)以及提供ThreadStart指派的worker方法。利用worker方法即可讀取所提供的參數(shù),因?yàn)樗镁褪荰ask對(duì)象的成員所以對(duì)線程當(dāng)然是唯一的。通過(guò)令線程成為一種公共字段,你就可以獲得訪問(wèn)線程所有成員的權(quán)限而不必編寫(xiě)額外的封裝代碼了。請(qǐng)參看程序清單B 閱讀這一技術(shù)的有關(guān)示例。
程序清單B
using System;
using System.Threading;
namespace TaskDemo {
public class MyTask {
public Thread m_thread;
string m_name;
public MyTask(string name) {
m_name = name;
m_thread = new Thread(new ThreadStart(Worker));
}
private void Worker() {
Console.WriteLine("Hello, ");
Thread.Sleep(1500);
Console.WriteLine(m_name);
}
}
class TaskDemo1 {
static void Main(string [] args){
MyTask task1 = new MyTask("Bill");
MyTask task2 = new MyTask("Steve");
task1.m_thread.Start();
task2.m_thread.Start();
}
}
}
你甚至可以通過(guò)在保存線程的任務(wù)中定義字段的方法提供Task對(duì)象的某種返回值,在線程完成前設(shè)置這一返回值,最后在這項(xiàng)任務(wù)完成以后從啟動(dòng)這項(xiàng)任務(wù)的線程讀取它。
你可以暫停一個(gè)線程、等待其他線程完成其任務(wù)。你可以在打算采集返回結(jié)果的時(shí)候執(zhí)行兩種操作,在三個(gè)分隔的線程之間執(zhí)行數(shù)據(jù)庫(kù)更新但直到所有線程都結(jié)束時(shí)才想進(jìn)行數(shù)據(jù)處理也可以采用以上兩種操作。該技術(shù)如程序清單C所示。
程序清單C:
http://builder.com.com/utils/sidebar.jhtml?id=u00220020531pcb01.htm&index=3
這里,我們采用了程序清單A的代碼創(chuàng)建程序。這次我們運(yùn)行兩個(gè)線程,每一個(gè)線程完成同以前一樣的任務(wù)。調(diào)用兩個(gè)線程的Start () 方法之后調(diào)用它們的Join()方法。對(duì)線程調(diào)用Join()方法會(huì)令調(diào)用線程暫停執(zhí)行直到被調(diào)用線程結(jié)束。因此thread1.Start ()方法會(huì)令主線程暫停直到thread1完成。然后我們對(duì)thread2執(zhí)行同樣的操作。結(jié)果,主線程直到thread1和 thread2都完成了才最后完成。
這個(gè)例子的思想分為兩部分。首先,某一個(gè)線程不能調(diào)用另一線程上的Join方法除非后者已經(jīng)啟動(dòng)。第二,有多于兩種形式的Join可以設(shè)定調(diào)用線程繼續(xù)運(yùn)行的超時(shí)時(shí)間哪怕被調(diào)用線程仍在運(yùn)行。
計(jì)算機(jī)科學(xué)中經(jīng)常會(huì)提到看門(mén)狗概念,所謂看門(mén)狗(watchdog)其實(shí)就是負(fù)責(zé)保證功能正確性或者處理不正確功能的實(shí)體。另一種實(shí)體,也就是常用的看門(mén)狗計(jì)時(shí)器(watchdog timer)則通常負(fù)責(zé)保證另一任務(wù)在合理的時(shí)間內(nèi)按時(shí)完成。程序清單D所示就是實(shí)現(xiàn)看門(mén)狗計(jì)時(shí)器的簡(jiǎn)單實(shí)現(xiàn)機(jī)制。
程序清單D:
http://builder.com.com/utils/sidebar.jhtml?id=u00220020531pcb01.htm&index=4
thread1啟動(dòng)之后我們就加入該線程但提供了10秒鐘的超時(shí)時(shí)間。因?yàn)閠hread1內(nèi)置15秒暫停設(shè)置,所以在加入超期之后還會(huì)繼續(xù)存活。主線程則測(cè)試thread1.IsAlive,如果它還活動(dòng)則終止線程。
同步和監(jiān)視器
同步指的是保證一次一節(jié)代碼中只有一個(gè)線程在執(zhí)行的措施。討論同步技術(shù)已經(jīng)超出了本文所涉及的主題范圍,但單個(gè)線程模塊之內(nèi)為可靠起見(jiàn)實(shí)際上會(huì)產(chǎn)生為數(shù)不少的構(gòu)造。然而,它們中的大多數(shù)在這些代碼塊的外部,在大多數(shù)時(shí)間內(nèi)都工作很正常,這樣,我們一直所熟知的“如果編譯通過(guò)并且得到我期望的答案那么它就是正確的”這種說(shuō)法在這里就不一定成立了。這就是多線程為什么如此危險(xiǎn)的部分原因。
監(jiān)視器是最基礎(chǔ)的同步構(gòu)造。任何對(duì)象都有自己關(guān)聯(lián)的監(jiān)視器,一個(gè)監(jiān)視器只能分配給一個(gè)對(duì)象。監(jiān)視器上有一個(gè)“鎖(lock)”,這個(gè)鎖可以在某一時(shí)刻被唯一線程獲得。在另一線程可以獲得這把鎖之前必須先釋放它。你可以聲明某個(gè)對(duì)象對(duì)所有線程可見(jiàn)來(lái)保護(hù)某一段代碼,比如類字段等。你還可以在實(shí)施某種操作之前讓某段代碼從監(jiān)視器那里獲得對(duì)象鎖,然后在操作完成之后釋放這把鎖。該構(gòu)造的示范如程序清單E所示。
程序清單E:
http://builder.com.com/utils/sidebar.jhtml?id=u00220020531pcb01.htm&index=5
這里我們聲明了一個(gè)對(duì)象myLockObject,該對(duì)象的唯一目的就是提供同步監(jiān)視器。在 SayHello方法中,無(wú)論何時(shí)只要需要我們就允許兩個(gè)線程打印出“Hello”的字樣。然而,現(xiàn)在我們就通過(guò)myMonitorObject所關(guān)聯(lián)的監(jiān)視器控制了“Wonderful”和“World”的打印,這樣,一個(gè)線程在被允許開(kāi)始打印之前另一線程必須完成兩次打印。
實(shí)現(xiàn)以上機(jī)制還可以采用另兩種技術(shù)——lock()關(guān)鍵詞何MethodImplAttribute屬性。示例請(qǐng)參看程序清單F。
程序清單F:
http://builder.com.com/utils/sidebar.jhtml?id=u00220020531pcb01.htm&index=6
我們用lock(…){ … }代替Monitor.Enter(…) and Monitor.Exit(…)構(gòu)造。這些構(gòu)造在效果上是相同的,只不過(guò)后者相比前者更為便捷些。我們還增加了一個(gè)方法SayHello2 (),它具有屬性MethodImpl。這一屬性指定了將被同步的全部方法。實(shí)質(zhì)等價(jià)于在包含這些同步方法的類型對(duì)象被允許調(diào)用方法之前強(qiáng)迫調(diào)用代碼獲取類型對(duì)象關(guān)聯(lián)的監(jiān)視器上的鎖。這比在lock(){…}語(yǔ)句中封裝方法代碼可清楚多了。注意,文檔中定義該屬性為MethodImplAttribute,但它的實(shí)現(xiàn)卻叫做MethodImpl。根據(jù)聲明屬性的陳述習(xí)慣,也許微軟的某個(gè)開(kāi)發(fā)人員自己可能沒(méi)注意到這一疏忽。
小結(jié)
這篇文章的內(nèi)容涵蓋很多方面的問(wèn)題。我已經(jīng)討論了采用或者不采用多線程技術(shù)的若干理由,同時(shí)還展示了某些用在多線程編程中的原語(yǔ)構(gòu)造。此外我還介紹了線程對(duì)象并解釋了運(yùn)行若干線程完成任務(wù)的原理、什么是監(jiān)視器以及如何通過(guò)監(jiān)視器的使用完成代碼的同步。在特定的情況下,lock關(guān)鍵詞和MethodImpl屬性完成同樣的工作。
在后續(xù)的文章里我將繼續(xù)描述其它基本構(gòu)造,實(shí)現(xiàn)一個(gè)線程池,并且探討更多的構(gòu)造類型, 例如線程本地存儲(chǔ)和重疊I/O等。