原文地址:C#多線程之旅(1)——介紹和基本概念
C#多線程之旅目錄:
C#多線程之旅(5)——同步機制介紹
C#多線程之旅(6)——詳解多線程中的鎖
更多文章正在更新中,敬請期待......
C#多線程之旅(1)——介紹和基本概念
一、多線程介紹
C#通過多線程支持並行執行的代碼。一個線程是一個獨立執行的路徑,可以同時與其他線程一起運行。一個C#客戶端程序(Console,WPF,Winows Forms)開始於一個單獨的線程,該線程由CLR和操作系統自動地創建,我們稱它為主線程,而且可以通過創建附加的線程來實現多線程。
所有的例子都假設引入了以下的namespaces:
Using System;
Using System.Threading;
1.初探
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 Thread thread = new Thread(WriteY);//創建一個線程 6 thread.Start();//開始一個線程 7 8 for (int i = 0; i < 1000; i++)//主線程執行循環 9 { 10 Console.Write("x"); 11 } 12 13 Console.ReadLine(); 14 } 15 static void WriteY() 16 { 17 for (int i = 0; i < 1000; i++) 18 { 19 Console.Write("y"); 20 } 21 } 22 23 }
一旦開始,一個線程的IsAlive屬性返回true,直到這個線程結束。當傳遞給線程的構造函數的委托完成執行時,這個線程結束。一旦結束,這個線程不能重啟。
2.內存隔離
CLR給每個線程分配自己內存棧,因此局部變量可以保持分離。在下一個例子中,我們定義了一個
使用局部變量的方法,然后在主線程和子線程同時調用這個方法。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 new Thread(Go).Start(); 6 Go(); 7 Console.ReadKey(); 8 } 9 10 static void Go() 11 { 12 for (int i = 0; i < 5; i++) 13 { 14 Console.Write("y"); 15 } 16 } 17 }
因為每個線程的內存棧都有一份隔離的循環變量的拷貝,因此可以推斷出,輸出結果是10個“y”字符 。
3.數據共享
如果多個線程對同一個對象實例有相同的引用,這些線程就共享這個對象實例的數據。例如:
1 class Program 2 { 3 bool done = false; 4 static void Main(string[] args) 5 { 6 Program p= new Program(); 7 new Thread(p.Go).Start(); 8 p.Go(); 9 Console.ReadKey(); 10 } 11 12 void Go() 13 { 14 if (!done) 15 { 16 done = true; 17 Console.WriteLine("Done"); 18 } 19 } 20 }
因為兩個線程都調用實例p的go的方法,因此他們共享done這個字段,結果是done只打印出一次而不是兩次。
靜態字段提供另外一種共享數據的方法:
1 class ThreadTest 2 { 3 static bool done; // Static fields are shared between all threads 4 5 static void Main() 6 { 7 new Thread (Go).Start(); 8 Go(); 9 } 10 11 static void Go() 12 { 13 if (!done) { done = true; Console.WriteLine ("Done"); } 14 } 15 }
4.線程安全
這兩個例子展示了另外一個重要的概念:線程安全確實是不確定的:done可能被打印出兩次(盡管是不太可能發生的)。當我們把Go方法中的語句的順序交換下,打印出兩次done的幾率顯著提升。
1 class Program 2 { 3 static bool done = false; 4 static void Main(string[] args) 5 { 6 Program p = new Program(); 7 new Thread(p.Go).Start(); 8 p.Go(); 9 Console.ReadKey(); 10 } 11 12 void Go() 13 { 14 if (!done) 15 { 16 Console.WriteLine("Done"); 17 done = true; 18 } 19 } 20 }
這個地方的問題是線程A在線程B設置done等於true之前進入if條件判斷中,所有A有機會打印出"Done"。
改進方式當讀\寫一個公共字段時,獲取一個獨占鎖(exclusive lock)。C#提供了關鍵字lock。
1 class Program 2 { 3 static bool done = false; 4 static readonly object locker = new object(); 5 static void Main(string[] args) 6 { 7 new Thread(Go).Start(); 8 Go(); 9 Console.ReadKey(); 10 } 11 12 static void Go() 13 { 14 lock (locker) 15 { 16 if (!done) 17 { 18 Console.WriteLine("Done"); 19 done = true; 20 } 21 } 22 } 23 }
當兩個線程同時搶占一個鎖時(在這個例子中,locker),一個線程等待,或者阻塞,知道這個鎖釋放。在這個例子中,這個鎖保證一次只有一個線程可以進入代碼的臨界區域,然后“Done”只會被打印一次。代碼在這種不確定的多線程背景下中被保護被叫做線程安全。
注意:在多線程中,共享數據是造成復雜原因的主要,而且會產生讓人費解的錯誤。盡管很基本但還是要盡可能保持簡單。
一個線程,當阻塞的時候,不占用CPU資源。
二、Join 和Sleep
1.Join
通過調用一個線程的Join方法,可以等待另外一個線程結束。例如:
1 static void Main(string[] args) 2 { 3 Thread t = new Thread(Go); 4 t.Start(); 5 t.Join(); 6 Console.WriteLine("Thread t has ended!"); 7 Console.ReadKey(); 8 9 } 10 static void Go() 11 { 12 for (int i = 0; i < 1000; i++) 13 { 14 Console.Write("y"); 15 } 16 }
這個會打印字符"y"1000次,然后緊接着立刻打印"Thread t has ended!"。Join有多個重載方法,可以在Join方法中添加一個參數,milliseconds或者timeSpan。如果這個線程結束了則Join方法返回true,如果這個線程超時則返回false。
2.Sleep
Thread.Sleep暫停當前線程一段指定的時間:
Thread.Sleep(TimeSpan.FromHours(1));//sleep一個小時
Thread.Sleep(500);//sleep 500 微秒
當使用Sleep或Join暫停線程時,這個線程是阻塞的,不消耗CPU資源。
Thread.Sleep(0)立即放棄這個線程的時間片,主動交出CPU給其他線程。Framework 4.0的新方法Thread.Yield()方法做同樣的事,除了當它僅僅在同一個進程中時,才會放棄時間片。
Sleep(0)或Yield()有時候對提升產品性能有用。而且它們也是診斷工具可以幫助揭開線程安全的問題;
如果在代碼中的任何地方都插入Thread.Yield(),會造成bug。
三、線程怎樣工作
1.多線程由一個線程調度器來進行內部管理,一個功能是CLR常常委托給操做系統。
一個線程調度器確保所有激活的線程在執行期間被合適的分配,等待或者阻塞的線程(比如,一個獨占鎖或者等待用戶輸入)不占用CPU資源。
2.在單核電腦上,一個線程調度器讓時間片在每一個激活的線程中切換。在windows操作系統下,線程切換的時間分片通常為10微秒,遠遠大於CPU的開銷時間(通常小於1微秒)。
3.在一個多核的電腦上,多線程實現了一個混合的時間片和真正的並發,不同的線程同時在不同的CPU上執行代碼。還是存在某些時間片,因為操作系統需要服務它自己的線程,包括其他的應用的線程。
4.當一個線程的執行被內部因素打斷,比如時間片,則說這個線程是搶占式的。在大部分情形下,一個線程不能控制自己何時何地被搶占。
四、線程和進程
一個線程類似於你的應用程序正在運行的一個操作系統進程。類似於進程並行運行在一台電腦上,線程並行運行在一個單獨的進程中。進程之間是完全隔離的;線程在一定程度上隔離。運行在同一個應用程序下的線程共享堆內存。在某種程度上,這就是為什么線程如此有用:一個線程可以在后台取回數據,比如同時另外一個線程正在顯示數據。
五、線程的使用和誤用
多線程有許多用途,下面是最通用的:
保持一個可響應的用戶界面
通過在一個並行的“worker”線程上運行時間消耗的任務,主UI線程可以空閑地執行鍵盤或鼠標事件。
使其他阻塞CPU的線程得到最有效的使用
當一個線程正等待另外一計算機或硬件的響應時是非常有用的。當一個線程執行任務時阻塞了,其他線程正好可以使用計算機。
並行編程
如果工作負荷被共享給正在執行“各個擊破”策略的多個線程,則代碼在多核或多進程中集中計算可以執行得更快。
預測執行
在多核的機器上,你有時通過預測某些事情需要做,然后提前做,從而可以提高性能。LINQPad使用這項技術提高查詢的創建。一個變體是運行許多並行的算法去處理同樣的任務。無論哪個完成了第一個“wins”-當你預先不知道哪一個算法執行得更快時,這是非常有效的。
允許同時執行請求
在一個server上,客戶端請求可以並行抵達,所以需要並行處理。如果你使用ASP.NET,WCF,Web Service或Remoting,.NET Framework 會自動創建線程。這個在client上也是有用的(比如說處理點對點的net working,或者是user的多個請求)。
比如ASP.NET和WCF技術,你可能甚至不會注意到,除非你訪問沒有合適的locking,違反線程安全的共享數據(假定通過靜態字段)。
多線程會帶來一系列問題。最大的問題是多線程會提升復雜性。有許多線程本身不會帶來復雜性,而是因為線程之間的相互影響(尤其是通過共享數據)。這個適用於是否這個相互影響是故意的,而且這個可以造成長時間的開發周期和一個持續性的敏感性和不可重現的bug。因為這個原因,需要將相互影響降到最低。盡可能堅持和提高可靠的設計。這篇文章主要集中在處理這些復雜性,移除相互影響這個不用多說。
一個好的策略是封裝多線程的logic到可復用的類中,這些類可以獨立地被測試。這個Framework它自己提供了許多的高級線程構造函數,我們后面再介紹。
線程在調度和切換線程時會造成資源和CPU的消耗(當激活的線程數量多余CPU的核的數量時)-而且有創建/銷毀損耗。多線程通常會提升應用程序的速度-但是如果過度或者不適當使用甚至會使應用程序變慢。比如,當硬件I/O被涉及到時,有兩個線程串行運行任務比起10個並行線程一次性執行更快。(在等待和脈沖信號中,我們描述怎樣實現一個生產者/消費者隊列來實現這個功能。)
參考資料:《C# 4.0 in a Nutshell》