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