1 簡介及概念
·C# 支持通過多線程并行執(zhí)行代碼,線程有其獨立的執(zhí)行路徑胖翰,能夠與其它線程同時執(zhí)行。
·一個 C# 客戶端程序(Console 命令行、WPF 以及 Windows Forms)開始于一個單線程碑韵,這個線程(也稱為“主線程”)是由 CLR 和操作系統(tǒng)自動創(chuàng)建的撬讽,并且也可以再創(chuàng)建其它線程蕊连。以下是一個簡單的使用多線程的例子:
所有示例都假定已經(jīng)引用了以下命名空間:
>using System;
>using System.Threading;
class ThreadTest
{
static void Main()
{
Thread t = new Thread(WriteY); // 創(chuàng)建新線程
t.Start(); // 啟動新線程,執(zhí)行WriteY()
// 同時,在主線程做其它事情
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)建了一個新線程t來不斷打印字母 “ y “,與此同時糠馆,主線程在不停打印字母 “ x “赋焕。
線程一旦啟動,線程的IsAlive屬性值就會為true,直到線程結(jié)束。當傳遞給Thread的構(gòu)造方法的委托執(zhí)行完成時,線程就會結(jié)束囚聚。一旦結(jié)束,該線程不能再重新啟動标锄。
CLR 為每個線程分配各自獨立的椡缰空間,因此局部變量是獨立的料皇。在下面的例子中谓松,我們定義一個擁有局部變量的方法,然后在主線程和新創(chuàng)建的線程中同時執(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)建的毒返,因此才會輸出 10 個問號。
線程可以通過對同一對象的引用來共享數(shù)據(jù)舷手。例如:
class ThreadTest
{
bool done;
static void Main()
{
ThreadTest tt = new ThreadTest(); // 創(chuàng)建一個公共的實例
new Thread(tt.Go).Start();
tt.Go();
}
// 注意: Go現(xiàn)在是一個實例方法
void Go()
{
if (!done) { done = true; Console.WriteLine("Done"); }
}
}
由于兩個線程是調(diào)用了同一個的ThreadTest實例上的Go()拧簸,它們共享了done字段,因此輸出結(jié)果是一次 “ Done “男窟,而不是兩次盆赤。
輸出結(jié)果:Done
靜態(tài)字段提供了另一種在線程間共享數(shù)據(jù)的方式,以下是一個靜態(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"); }
}
}
以上兩個例子引出了一個關(guān)鍵概念線程安全(thread safety)歉眷。上述兩個例子的輸出實際上是不確定的:” Done “ 有可能會被打印兩次牺六。如果在Go
方法里調(diào)換指令的順序,” Done “ 被打印兩次的幾率會大幅提高:
static void Go()
{
if (!done) { Console.WriteLine("Done"); done = true; }
}
輸出結(jié)果:
Done
Done(很可能!)
這個問題是因為一個線程對if中的語句估值的時候汗捡,另一個線程正在執(zhí)行WriteLine語句淑际,這時done還沒有被設(shè)置為true畏纲。
修復這個問題需要在讀寫公共字段時,獲得一個排它鎖(互斥鎖春缕,exclusive lock )盗胀。C# 提供了lock來達到這個目的:
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; }
}
}
}
當兩個線程同時爭奪一個鎖的時候(例子中的locker),一個線程等待锄贼,或者說阻塞票灰,直到鎖變?yōu)榭捎谩_@樣就確保了在同一時刻只有一個線程能進入臨界區(qū)(critical section宅荤,不允許并發(fā)執(zhí)行的代碼)屑迂,所以 “ Done “ 只被打印了一次。像這種用來避免在多線程下的不確定性的方式被稱為線程安全(thread-safe)冯键。
在線程間共享數(shù)據(jù)是造成多線程復雜惹盼、難以定位的錯誤的主要原因。盡管這通常是必須的惫确,但應該盡可能保持簡單逻锐。
一個線程被阻塞時,不會消耗 CPU 資源雕薪。
1.1 Join 和 Sleep
可以通過調(diào)用Join方法來等待另一個線程結(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 次之后晓淀,緊接著會輸出 “ Thread t has ended! “所袁。當調(diào)用Join時可以使用一個超時參數(shù),以毫秒或是TimeSpan形式凶掰。如果線程正常結(jié)束則返回true燥爷,如果超時則返回false。
Thread.Sleep會將當前的線程阻塞一段時間:
Thread.Sleep (TimeSpan.FromHours (1)); // 阻塞 1小時
Thread.Sleep (500); // 阻塞 500 毫秒
當使用Sleep或Join等待時懦窘,線程是阻塞(blocked)狀態(tài)前翎,因此不會消耗 CPU 資源。
Thread.Sleep(0)會立即釋放當前的時間片畅涂,將 CPU 資源出讓給其它線程港华。Framework 4.0 新的Thread.Yield()方法與其相同,除了它只會出讓給運行在相同處理器核心上的其它線程午衰。
Sleep(0)和Yield在調(diào)整代碼性能時偶爾有用立宜,它也是一個很好的診斷工具,可以用于找出線程安全(thread safety)的問題臊岸。如果在你代碼的任意位置插入Thread.Yield()會影響到程序橙数,基本可以確定存在 bug。
1.2 線程是如何工作的
線程在內(nèi)部由一個線程調(diào)度器(thread scheduler)管理帅戒,一般 CLR 會把這個任務交給操作系統(tǒng)完成灯帮。線程調(diào)度器確保所有活動的線程能夠分配到適當?shù)膱?zhí)行時間,并且保證那些處于等待或阻塞狀態(tài)(例如,等待排它鎖或者用戶輸入)的線程不消耗CPU時間钟哥。
在單核計算機上迎献,線程調(diào)度器會進行時間切片(time-slicing),快速的在活動線程中切換執(zhí)行瞪醋。在 Windows 操作系統(tǒng)上忿晕,一個時間片通常在十幾毫秒(譯者注:默認 15.625ms),遠大于 CPU 在線程間進行上下文切換的開銷(通常在幾微秒?yún)^(qū)間)银受。
在多核計算機上践盼,多線程的實現(xiàn)是混合了時間切片和真實的并發(fā),不同的線程同時運行在不同的 CPU 核心上宾巍。幾乎可以肯定仍然會使用到時間切片咕幻,因為操作系統(tǒng)除了要調(diào)度其它的應用,還需要調(diào)度自身的線程顶霞。
線程的執(zhí)行由于外部因素(比如時間切片)被中斷稱為被搶占(preempted)肄程。在大多數(shù)情況下,線程無法控制其在何時及在什么代碼處被搶占选浑。
1.3 線程 vs 進程
好比多個進程并行在計算機上執(zhí)行蓝厌,多個線程是在一個進程中并行執(zhí)行。進程是完全隔離的古徒,而線程是在一定程度上隔離拓提。一般的,線程與運行在相同程序中的其它線程共享堆內(nèi)存隧膘。這就是線程為何有用的部分原因代态,一個線程可以在后臺獲取數(shù)據(jù),而另一個線程可以同時顯示已獲取到的數(shù)據(jù)疹吃。
1.4線程的使用與誤用
多線程有許多用處蹦疑,下面是通常的應用場景:
維持用戶界面的響應
使用工作線程并行運行時間消耗大的任務,這樣主UI線程就仍然可以響應鍵盤萨驶、鼠標的事件歉摧。
有效利用 CPU
多線程在一個線程等待其它計算機或硬件設(shè)備響應時非常有用。當一個線程在執(zhí)行任務時被阻塞腔呜,其它線程就可以利用這個空閑出來的CPU核心判莉。
并行計算
在多核心或多處理器的計算機上,計算密集型的代碼如果通過分治策略(divide-and-conquer育谬,見第 5 部分)將工作量分攤到多個線程券盅,就可以提高計算速度。
推測執(zhí)行(speculative execution)
在多核心的計算機上膛檀,有時可以通過推測之后需要被執(zhí)行的工作锰镀,提前執(zhí)行它們來提高性能娘侍。LINQPad就使用了這個技術(shù)來加速新查詢的創(chuàng)建。另一種方式就是可以多線程并行運行解決相同問題的不同算法泳炉,因為預先不知道哪個算法更好憾筏,這樣做就可以盡早獲得結(jié)果。
允許同時處理請求
在服務端花鹅,客戶端請求可能同時到達氧腰,因此需要并行處理(如果你使用 ASP.NET、WCF刨肃、Web Services 或者 Remoting古拴,.NET Framework 會自動創(chuàng)建線程)。這在客戶端同樣有用真友,例如處理 P2P 網(wǎng)絡(luò)連接黄痪,或是處理來自用戶的多個請求。
如果使用了 ASP.NET 和 WCF 之類的技術(shù)盔然,可能不會注意到多線程被使用桅打,除非是訪問共享數(shù)據(jù)時(比如通過靜態(tài)字段共享數(shù)據(jù))。如果沒有正確的加鎖愈案,就可能產(chǎn)生線程安全問題挺尾。
多線程同樣也會帶來缺點,最大的問題是它提高了程序的復雜度站绪。使用多個線程本身并不復雜潦嘶,復雜的是線程間的交互(一般是通過共享數(shù)據(jù))。無論線程間的交互是否有意為之崇众,都會帶來較長的開發(fā)周期,以及帶來間歇的航厚、難以重現(xiàn)的 bug顷歌。因此,最好保證線程間的交互盡量少幔睬,并堅持簡單和已被證明的多線程交互設(shè)計眯漩。這篇文章主要就是關(guān)于如何處理這種復雜的問題,如果能夠移除線程間交互麻顶,那會輕松許多赦抖。
一個好的策略是把多線程邏輯使用可重用的類封裝,以便于獨立的檢驗和測試辅肾。.NET Framework 提供了許多高層的線程構(gòu)造队萤,之后會講到。
當頻繁地調(diào)度和切換線程時(并且如果活動線程數(shù)量大于 CPU 核心數(shù))矫钓,多線程會增加資源和 CPU 的開銷要尔,線程的創(chuàng)建和銷毀也會增加開銷舍杜。多線程并不總是能提升程序的運行速度,如果使用不當赵辕,反而可能降低速度既绩。 例如,當需要進行大量的磁盤 I/O 時还惠,幾個工作線程順序執(zhí)行可能會比 10 個線程同時執(zhí)行要快饲握。(在使用Wait和Pulse進行同步中,將會描述如何實現(xiàn) 生產(chǎ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