1 簡(jiǎn)介及概念
·C# 支持通過(guò)多線程并行執(zhí)行代碼,線程有其獨(dú)立的執(zhí)行路徑,能夠與其它線程同時(shí)執(zhí)行。
·一個(gè) C# 客戶端程序(Console 命令行、WPF 以及 Windows Forms)開始于一個(gè)單線程,這個(gè)線程(也稱為“主線程”)是由 CLR 和操作系統(tǒng)自動(dòng)創(chuàng)建的,并且也可以再創(chuàng)建其它線程。以下是一個(gè)簡(jiǎn)單的使用多線程的例子:
所有示例都假定已經(jīng)引用了以下命名空間:
>using System;
>using System.Threading;
class ThreadTest
{
static void Main()
{
Thread t = new Thread(WriteY); // 創(chuàng)建新線程
t.Start(); // 啟動(dòng)新線程,執(zhí)行WriteY()
// 同時(shí),在主線程做其它事情
for (int i = 0; i < 1000; i++) Console.Write("x");
}
static void WriteY()
{
for (int i = 0; i < 1000; i++) Console.Write("y");
}
}
輸出結(jié)果:
xxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyy
yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyxxxxxxxxxxxxxxxxxxxxxx
...
主線程創(chuàng)建了一個(gè)新線程t來(lái)不斷打印字母 “ y “,與此同時(shí),主線程在不停打印字母 “ x “。

線程一旦啟動(dòng),線程的IsAlive屬性值就會(huì)為true,直到線程結(jié)束。當(dāng)傳遞給Thread的構(gòu)造方法的委托執(zhí)行完成時(shí),線程就會(huì)結(jié)束。一旦結(jié)束,該線程不能再重新啟動(dòng)。
CLR 為每個(gè)線程分配各自獨(dú)立的棧空間,因此局部變量是獨(dú)立的。在下面的例子中,我們定義一個(gè)擁有局部變量的方法,然后在主線程和新創(chuàng)建的線程中同時(shí)執(zhí)行該方法。
static void Main()
{
new Thread(Go).Start(); // 在新線程執(zhí)行Go()
Go(); // 在主線程執(zhí)行Go()
}
static void Go()
{
// 定義和使用局部變量 - 'cycles'
for (int cycles = 0; cycles < 5; cycles++) Console.Write('?');
}
輸出結(jié)果:??????????
變量cycles的副本是分別在各自的棧中創(chuàng)建的,因此才會(huì)輸出 10 個(gè)問(wèn)號(hào)。
線程可以通過(guò)對(duì)同一對(duì)象的引用來(lái)共享數(shù)據(jù)。例如:
class ThreadTest
{
bool done;
static void Main()
{
ThreadTest tt = new ThreadTest(); // 創(chuàng)建一個(gè)公共的實(shí)例
new Thread(tt.Go).Start();
tt.Go();
}
// 注意: Go現(xiàn)在是一個(gè)實(shí)例方法
void Go()
{
if (!done) { done = true; Console.WriteLine("Done"); }
}
}
由于兩個(gè)線程是調(diào)用了同一個(gè)的ThreadTest實(shí)例上的Go(),它們共享了done字段,因此輸出結(jié)果是一次 “ Done “,而不是兩次。
輸出結(jié)果:Done
靜態(tài)字段提供了另一種在線程間共享數(shù)據(jù)的方式,以下是一個(gè)靜態(tài)的done字段的例子:
class ThreadTest
{
static bool done; // 靜態(tài)字段在所有線程中共享
static void Main()
{
new Thread(Go).Start();
Go();
}
static void Go()
{
if (!done) { done = true; Console.WriteLine("Done"); }
}
}
以上兩個(gè)例子引出了一個(gè)關(guān)鍵概念線程安全(thread safety)。上述兩個(gè)例子的輸出實(shí)際上是不確定的:” Done “ 有可能會(huì)被打印兩次。如果在Go
方法里調(diào)換指令的順序,” Done “ 被打印兩次的幾率會(huì)大幅提高:
static void Go()
{
if (!done) { Console.WriteLine("Done"); done = true; }
}
輸出結(jié)果:
Done
Done(很可能!)
這個(gè)問(wèn)題是因?yàn)橐粋€(gè)線程對(duì)if中的語(yǔ)句估值的時(shí)候,另一個(gè)線程正在執(zhí)行WriteLine語(yǔ)句,這時(shí)done還沒有被設(shè)置為true。
修復(fù)這個(gè)問(wèn)題需要在讀寫公共字段時(shí),獲得一個(gè)排它鎖(互斥鎖,exclusive lock )。C# 提供了lock來(lái)達(dá)到這個(gè)目的:
class ThreadSafe
{
static bool done;
static readonly object locker = new object();
static void Main()
{
new Thread(Go).Start();
Go();
}
static void Go()
{
lock (locker)
{
if (!done) { Console.WriteLine("Done"); done = true; }
}
}
}
當(dāng)兩個(gè)線程同時(shí)爭(zhēng)奪一個(gè)鎖的時(shí)候(例子中的locker),一個(gè)線程等待,或者說(shuō)阻塞,直到鎖變?yōu)榭捎?。這樣就確保了在同一時(shí)刻只有一個(gè)線程能進(jìn)入臨界區(qū)(critical section,不允許并發(fā)執(zhí)行的代碼),所以 “ Done “ 只被打印了一次。像這種用來(lái)避免在多線程下的不確定性的方式被稱為線程安全(thread-safe)。
在線程間共享數(shù)據(jù)是造成多線程復(fù)雜、難以定位的錯(cuò)誤的主要原因。盡管這通常是必須的,但應(yīng)該盡可能保持簡(jiǎn)單。
一個(gè)線程被阻塞時(shí),不會(huì)消耗 CPU 資源。
1.1 Join 和 Sleep
可以通過(guò)調(diào)用Join方法來(lái)等待另一個(gè)線程結(jié)束,例如:
static void Main()
{
Thread t = new Thread(Go);
t.Start();
t.Join();
Console.WriteLine("Thread t has ended!");
}
static void Go()
{
for (int i = 0; i < 1000; i++) Console.Write("y");
}
輸出 “ y “ 1,000 次之后,緊接著會(huì)輸出 “ Thread t has ended! “。當(dāng)調(diào)用Join時(shí)可以使用一個(gè)超時(shí)參數(shù),以毫秒或是TimeSpan形式。如果線程正常結(jié)束則返回true,如果超時(shí)則返回false。
Thread.Sleep會(huì)將當(dāng)前的線程阻塞一段時(shí)間:
Thread.Sleep (TimeSpan.FromHours (1)); // 阻塞 1小時(shí)
Thread.Sleep (500); // 阻塞 500 毫秒
當(dāng)使用Sleep或Join等待時(shí),線程是阻塞(blocked)狀態(tài),因此不會(huì)消耗 CPU 資源。
Thread.Sleep(0)會(huì)立即釋放當(dāng)前的時(shí)間片,將 CPU 資源出讓給其它線程。Framework 4.0 新的Thread.Yield()方法與其相同,除了它只會(huì)出讓給運(yùn)行在相同處理器核心上的其它線程。
Sleep(0)和Yield在調(diào)整代碼性能時(shí)偶爾有用,它也是一個(gè)很好的診斷工具,可以用于找出線程安全(thread safety)的問(wèn)題。如果在你代碼的任意位置插入Thread.Yield()會(huì)影響到程序,基本可以確定存在 bug。
1.2 線程是如何工作的
線程在內(nèi)部由一個(gè)線程調(diào)度器(thread scheduler)管理,一般 CLR 會(huì)把這個(gè)任務(wù)交給操作系統(tǒng)完成。線程調(diào)度器確保所有活動(dòng)的線程能夠分配到適當(dāng)?shù)膱?zhí)行時(shí)間,并且保證那些處于等待或阻塞狀態(tài)(例如,等待排它鎖或者用戶輸入)的線程不消耗CPU時(shí)間。
在單核計(jì)算機(jī)上,線程調(diào)度器會(huì)進(jìn)行時(shí)間切片(time-slicing),快速的在活動(dòng)線程中切換執(zhí)行。在 Windows 操作系統(tǒng)上,一個(gè)時(shí)間片通常在十幾毫秒(譯者注:默認(rèn) 15.625ms),遠(yuǎn)大于 CPU 在線程間進(jìn)行上下文切換的開銷(通常在幾微秒?yún)^(qū)間)。
在多核計(jì)算機(jī)上,多線程的實(shí)現(xiàn)是混合了時(shí)間切片和真實(shí)的并發(fā),不同的線程同時(shí)運(yùn)行在不同的 CPU 核心上。幾乎可以肯定仍然會(huì)使用到時(shí)間切片,因?yàn)椴僮飨到y(tǒng)除了要調(diào)度其它的應(yīng)用,還需要調(diào)度自身的線程。
線程的執(zhí)行由于外部因素(比如時(shí)間切片)被中斷稱為被搶占(preempted)。在大多數(shù)情況下,線程無(wú)法控制其在何時(shí)及在什么代碼處被搶占。
1.3 線程 vs 進(jìn)程
好比多個(gè)進(jìn)程并行在計(jì)算機(jī)上執(zhí)行,多個(gè)線程是在一個(gè)進(jìn)程中并行執(zhí)行。進(jìn)程是完全隔離的,而線程是在一定程度上隔離。一般的,線程與運(yùn)行在相同程序中的其它線程共享堆內(nèi)存。這就是線程為何有用的部分原因,一個(gè)線程可以在后臺(tái)獲取數(shù)據(jù),而另一個(gè)線程可以同時(shí)顯示已獲取到的數(shù)據(jù)。
1.4線程的使用與誤用
多線程有許多用處,下面是通常的應(yīng)用場(chǎng)景:
維持用戶界面的響應(yīng)
使用工作線程并行運(yùn)行時(shí)間消耗大的任務(wù),這樣主UI線程就仍然可以響應(yīng)鍵盤、鼠標(biāo)的事件。
有效利用 CPU
多線程在一個(gè)線程等待其它計(jì)算機(jī)或硬件設(shè)備響應(yīng)時(shí)非常有用。當(dāng)一個(gè)線程在執(zhí)行任務(wù)時(shí)被阻塞,其它線程就可以利用這個(gè)空閑出來(lái)的CPU核心。
并行計(jì)算
在多核心或多處理器的計(jì)算機(jī)上,計(jì)算密集型的代碼如果通過(guò)分治策略(divide-and-conquer,見第 5 部分)將工作量分?jǐn)偟蕉鄠€(gè)線程,就可以提高計(jì)算速度。
推測(cè)執(zhí)行(speculative execution)
在多核心的計(jì)算機(jī)上,有時(shí)可以通過(guò)推測(cè)之后需要被執(zhí)行的工作,提前執(zhí)行它們來(lái)提高性能。LINQPad就使用了這個(gè)技術(shù)來(lái)加速新查詢的創(chuàng)建。另一種方式就是可以多線程并行運(yùn)行解決相同問(wèn)題的不同算法,因?yàn)轭A(yù)先不知道哪個(gè)算法更好,這樣做就可以盡早獲得結(jié)果。
允許同時(shí)處理請(qǐng)求
在服務(wù)端,客戶端請(qǐng)求可能同時(shí)到達(dá),因此需要并行處理(如果你使用 ASP.NET、WCF、Web Services 或者 Remoting,.NET Framework 會(huì)自動(dòng)創(chuàng)建線程)。這在客戶端同樣有用,例如處理 P2P 網(wǎng)絡(luò)連接,或是處理來(lái)自用戶的多個(gè)請(qǐng)求。
如果使用了 ASP.NET 和 WCF 之類的技術(shù),可能不會(huì)注意到多線程被使用,除非是訪問(wèn)共享數(shù)據(jù)時(shí)(比如通過(guò)靜態(tài)字段共享數(shù)據(jù))。如果沒有正確的加鎖,就可能產(chǎn)生線程安全問(wèn)題。
多線程同樣也會(huì)帶來(lái)缺點(diǎn),最大的問(wèn)題是它提高了程序的復(fù)雜度。使用多個(gè)線程本身并不復(fù)雜,復(fù)雜的是線程間的交互(一般是通過(guò)共享數(shù)據(jù))。無(wú)論線程間的交互是否有意為之,都會(huì)帶來(lái)較長(zhǎng)的開發(fā)周期,以及帶來(lái)間歇的、難以重現(xiàn)的 bug。因此,最好保證線程間的交互盡量少,并堅(jiān)持簡(jiǎn)單和已被證明的多線程交互設(shè)計(jì)。這篇文章主要就是關(guān)于如何處理這種復(fù)雜的問(wèn)題,如果能夠移除線程間交互,那會(huì)輕松許多。
一個(gè)好的策略是把多線程邏輯使用可重用的類封裝,以便于獨(dú)立的檢驗(yàn)和測(cè)試。.NET Framework 提供了許多高層的線程構(gòu)造,之后會(huì)講到。
當(dāng)頻繁地調(diào)度和切換線程時(shí)(并且如果活動(dòng)線程數(shù)量大于 CPU 核心數(shù)),多線程會(huì)增加資源和 CPU 的開銷,線程的創(chuàng)建和銷毀也會(huì)增加開銷。多線程并不總是能提升程序的運(yùn)行速度,如果使用不當(dāng),反而可能降低速度。 例如,當(dāng)需要進(jìn)行大量的磁盤 I/O 時(shí),幾個(gè)工作線程順序執(zhí)行可能會(huì)比 10 個(gè)線程同時(shí)執(zhí)行要快。(在使用Wait和Pulse進(jìn)行同步中,將會(huì)描述如何實(shí)現(xiàn) 生產(chǎn)者 / 消費(fèi)者隊(duì)列,它提供了上述功能。)
參考文獻(xiàn):
http://www.codeproject.com/Articles/98346/Microsecond-and-Millisecond-NET-Timer
http://www.codeproject.com/Articles/571289/Obtaining-Microsecond-Precision-in-NET
http://www.pinvoke.net/default.aspx/winmm/timeSetEvent.html
http://www.geisswerks.com/ryan/FAQS/timing.html
http://blog.gkarch.com/topic/threading.html
http://omeg.pl/blog/2011/11/on-winapi-timers-and-their-resolution/
https://randomascii.wordpress.com/2013/07/08/windows-timer-resolution-megawatts-wasted/
http://www.windowstimestamp.com/description