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