C#多執行緒之旅目錄:
更多文章正在更新中,敬請期待……
C#多執行緒之旅(1)——介紹和基本概念
一、多執行緒介紹
C#通過多執行緒支援並行執行的程式碼。一個執行緒是一個獨立執行的路徑,可以同時與其他執行緒一起執行。一個C#客戶端程式(Console,WPF,Winows Forms)開始於一個單獨的執行緒,該執行緒由CLR和作業系統自動地建立,我們稱它為主執行緒,而且可以通過建立附加的執行緒來實現多執行緒。
所有的例子都假設引入了以下的namespaces:
Using System;
Using System.Threading;
1.初探
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Program { static void Main(string[] args) { Thread thread = new Thread(WriteY);//建立一個執行緒 thread.Start();//開始一個執行緒 for (int i = 0; i < 1000; i++)//主執行緒執行迴圈 { Console.Write("x"); } Console.ReadLine(); } static void WriteY() { for (int i = 0; i < 1000; i++) { Console.Write("y"); } } } |
一旦開始,一個執行緒的IsAlive屬性返回true,直到這個執行緒結束。當傳遞給執行緒的建構函式的委託完成執行時,這個執行緒結束。一旦結束,這個執行緒不能重啟。
2.記憶體隔離
CLR給每個執行緒分配自己記憶體棧,因此區域性變數可以保持分離。在下一個例子中,我們定義了一個
使用區域性變數的方法,然後在主執行緒和子執行緒同時呼叫這個方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Program { static void Main(string[] args) { new Thread(Go).Start(); Go(); Console.ReadKey(); } static void Go() { for (int i = 0; i < 5; i++) { Console.Write("y"); } } } |
因為每個執行緒的記憶體棧都有一份隔離的迴圈變數的拷貝,因此可以推斷出,輸出結果是10個“y”字元 。
3.資料共享
如果多個執行緒對同一個物件例項有相同的引用,這些執行緒就共享這個物件例項的資料。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Program { bool done = false; static void Main(string[] args) { Program p= new Program(); new Thread(p.Go).Start(); p.Go(); Console.ReadKey(); } void Go() { if (!done) { done = true; Console.WriteLine("Done"); } } } |
因為兩個執行緒都呼叫例項p的go的方法,因此他們共享done這個欄位,結果是done只列印出一次而不是兩次。
靜態欄位提供另外一種共享資料的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class ThreadTest { static bool done; // Static fields are shared between all threads static void Main() { new Thread (Go).Start(); Go(); } static void Go() { if (!done) { done = true; Console.WriteLine ("Done"); } } } |
4.執行緒安全
這兩個例子展示了另外一個重要的概念:執行緒安全確實是不確定的:done可能被列印出兩次(儘管是不太可能發生的)。當我們把Go方法中的語句的順序交換下,列印出兩次done的機率顯著提升。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Program { static bool done = false; static void Main(string[] args) { Program p = new Program(); new Thread(p.Go).Start(); p.Go(); Console.ReadKey(); } void Go() { if (!done) { Console.WriteLine("Done"); done = true; } } } |
這個地方的問題是執行緒A線上程B設定done等於true之前進入if條件判斷中,所有A有機會列印出“Done”。
改進方式當讀寫一個公共欄位時,獲取一個獨佔鎖(exclusive lock)。C#提供了關鍵字lock。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Program { static bool done = false; static readonly object locker = new object(); static void Main(string[] args) { new Thread(Go).Start(); Go(); Console.ReadKey(); } static void Go() { lock (locker) { if (!done) { Console.WriteLine("Done"); done = true; } } } } |
當兩個執行緒同時搶佔一個鎖時(在這個例子中,locker),一個執行緒等待,或者阻塞,知道這個鎖釋放。在這個例子中,這個鎖保證一次只有一個執行緒可以進入程式碼的臨界區域,然後“Done”只會被列印一次。程式碼在這種不確定的多執行緒背景下中被保護被叫做執行緒安全。
注意:在多執行緒中,共享資料是造成複雜原因的主要,而且會產生讓人費解的錯誤。儘管很基本但還是要儘可能保持簡單。
一個執行緒,當阻塞的時候,不佔用CPU資源。
二、Join 和Sleep
1.Join
通過呼叫一個執行緒的Join方法,可以等待另外一個執行緒結束。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static void Main(string[] args) { Thread t = new Thread(Go); t.Start(); t.Join(); Console.WriteLine("Thread t has ended!"); Console.ReadKey(); } static void Go() { for (int i = 0; i < 1000; i++) { Console.Write("y"); } } |
這個會列印字元“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個並行執行緒一次性執行更快。(在等待和脈衝訊號中,我們描述怎樣實現一個生產者/消費者佇列來實現這個功能。)