【转】C#中的线程 入门
Keywords:C#?線程
Source:http://www.albahari.com/threading/
Author: Joe Albahari
Translator: Swanky Wu
Published:?http://www.cnblogs.com/txw1958/
Download:http://www.albahari.info/threading/threading.pdf
?
?本系列文章可以算是一本很出色的C#線程手冊,思路清晰,要點都有介紹,看了后對C#的線程及同步等有了更深入的理解。
- 入門
- 概述與概念
- 創(chuàng)建和開始使用多線程
- 線程同步基礎(chǔ)
- 同步要領(lǐng)
- 鎖和線程安全
- Interrupt 和 Abort
- 線程狀態(tài)
- 等待句柄
- 同步環(huán)境
- 使用多線程
- 單元模式和Windows Forms
- BackgroundWorker類
- ReaderWriterLock類
- 線程池
- 異步委托
- 計時器
- 局部儲存
- 高級話題
- 非阻止同步
- Wait和Pulse
- Suspend和Resume
- 終止線程
一、入門
1. ? ? 概述與概念
?? C#支持通過多線程并行地執(zhí)行代碼,一個線程有它獨立的執(zhí)行路徑,能夠與其它的線程同時地運行(單CPU多核)。一個C#客戶端程序(Console, WPF, or Windows Forms)開始于一個單線程,這個單線程是被CLR和操作系統(tǒng)(也稱為“主線程”)自動創(chuàng)建的,并可以通過創(chuàng)建額外的線程 組成多線程。這里的一個簡單的例子及其輸出:
?????除非被指定,否則所有的例子都假定以下命名空間被引用了:??
??using System;?
?? using System.Threading;
?
class ThreadTest {static void Main() {Thread t = new Thread (WriteY);t.Start(); // Run WriteY on the new thread//同時,主線程做其他事情while (true) Console.Write ("x"); // Write 'x' forever }static void WriteY() {while (true) Console.Write ("y"); // Write 'y' forever } }主線程創(chuàng)建了一個新線程“t”,新線程它運行了一個重復(fù)打印字母"y"的方法,同時主線程重復(fù)打印字母“x”。
線程一旦開始,其 Islive屬性為true,直到線程結(jié)束。當委托傳遞給線程的構(gòu)造函數(shù)執(zhí)行完畢線程就結(jié)束,一旦結(jié)束,線程不能重新開始。
CLR分配每個線程到它自己的內(nèi)存堆棧上,來保證局部變量的分離運行。
在接下來的例子中,我們定義有一個局部變量的方法,然后在主線程和新創(chuàng)建的線程上同時地調(diào)用這個方法。
static void Main() {new Thread (Go).Start(); // Call Go() on a new threadGo(); // Call Go() on the main thread }static void Go() {// Declare and use a local variable - 'cycles'for (int cycles = 0; cycles < 5; cycles++) Console.Write ('?'); }
?變量cycles的副本分別在各自的內(nèi)存堆棧中創(chuàng)建,輸出也一樣,可預(yù)見,會有10個問號輸出。。
當兩個線程們引用了一些共同的對象實例的時候,他們會共享數(shù)據(jù)。下面是實例:
class ThreadTest {bool done; static void Main() {ThreadTest tt = new ThreadTest(); // Create a common instance (tt作為他們共同的一個實例對象)new Thread (tt.Go).Start(); //新線程調(diào)用tt.Go(); //主線程調(diào)用 }// Note that Go is now an instance method(實例方法)void Go() {if (!done) { done = true; Console.WriteLine ("Done"); }} } 因為兩個線程在同一樣的ThreadTest對象上 調(diào)用了Go(),它們<span style="color:#ff0000;">共享了done字段,這個結(jié)果輸出的是一個"Done",而不是兩個</span>。
靜態(tài)字段提供了另一種在線程間共享數(shù)據(jù)的方式,下面是一個以done為靜態(tài)字段的例子:
class ThreadTest {static bool done; // Static fields are shared between all threadsstatic void Main() {new Thread (Go).Start();Go();}static void Go() {if (!done) { done = true; Console.WriteLine ("Done"); }} }上述兩個例子足以說明, 另一個關(guān)鍵概念, 那就是線程安全。?輸出實際上是不確定的:它可能(雖然不大可能) ?"Done" 被打印兩次。然而,如果我們在Go方法里調(diào)換指令的順序, "Done"被打印兩次的機會會大幅地上升:(實踐證明是的,)
?
static void Go() {if (!done) { Console.WriteLine ("Done"); done = true; } }?
問題就是一個線程在判斷if塊的時候,正好另一個線程正在執(zhí)行WriteLine語句——在它將done設(shè)置為true之前。
補救措施是當讀寫公共字段的時候,提供一個互斥鎖;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),一個線程等待,或者說被阻止(blocks), 直到那個鎖變的可用。在這種情況下,就確保了在同一時刻只有一個線程能進入臨界區(qū),所以"Done"只被打印了1次(之后done為true)。代碼以如此方式在不確定的多線程環(huán)境中被叫做線程安全。
多線程中復(fù)雜而隱蔽錯誤的一個主要原因是數(shù)據(jù)共享。盡管多線程是經(jīng)常必須的,但盡可能保持簡單點。
A thread, while?blocked, doesn't consume CPU resources.(一個線程,阻塞的時候,不消耗CPU資源。)
(讓線程等一段時間再執(zhí)行)
你(主線程)可以等待另一個線程結(jié)束后再執(zhí)行?通過調(diào)用它(另一線程)的Join方法,例如: 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"); }
結(jié)果先打印1000次“y”,再打印Thread t has ended!,Join()可設(shè)置等待的時間,時間一到,其他線程就會執(zhí)行
Thread.Sleep?pauses the current thread for a specified period:(阻止當前線程一個指定的時間)
Thread.Sleep (TimeSpan.FromHours (1)); // sleep for 1 hour Thread.Sleep (500); // sleep for 500 milliseconds?線程是如何工作的
?? 線程被一個線程協(xié)調(diào)程序管理著(一個CLR委托給操作系統(tǒng)的函數(shù))。線程協(xié)調(diào)程序確保將所有活動的線程被分配適當?shù)膱?zhí)行時間;并且那些等待或阻止的線程—比如說在互斥鎖中、或在用戶輸入,都是不消耗CPU時間的。
?? 在單核處理器的電腦中,線程協(xié)調(diào)程序完成一個時間片之后迅速地在活動的線程之間進行切換執(zhí)行。這就導(dǎo)致“波濤洶涌”的行為,例如在第一個例子,每次重復(fù)的X 或 Y 塊相當于分給線程的時間片。在Windows XP中時間片通常在10毫秒內(nèi)選擇要比CPU開銷在處理線程切換的時候的消耗大的多。(即通常在幾微秒?yún)^(qū)間)
?? 在多核的電腦中,多線程被實現(xiàn)成混合時間片和真實的并發(fā)——不同的線程在不同的CPU上運行。這幾乎可以肯定仍然會出現(xiàn)一些時間切片, 由于操作系統(tǒng)的需要服務(wù)自己的線程,以及一些其他的應(yīng)用程序。
?? 線程由于外部因素(比如時間片)被中斷被稱為被搶占,在大多數(shù)情況下,一個線程方面在被搶占的那一時那一刻就失去了對它的控制權(quán)。
?? 線程 vs. 進程
????屬于一個單一的應(yīng)用程序的所有的線程邏輯上被包含在一個進程中,進程指一個應(yīng)用程序所運行的操作系統(tǒng)單元。
??? 線程于進程有某些相似的地方:正如進程并行運行在電腦上一樣, 線程并行運行在單個進程。。進程是完全獨立于彼此的。而線程只是一個有限程度的隔離。線程與運行在相同程序中的其它線程共享(堆heap)內(nèi)存,這就是線程為何如此有用:一個線程可以在后臺讀取數(shù)據(jù),而另一個線程可以在前臺展現(xiàn)已讀取的數(shù)據(jù)。
??線程的使用和誤用
?多線程有許多用途,下面是最常見的用途:
1、保持一個快速響應(yīng)的用戶界面
通過在一個并行的“worker”線程上運行耗時的任務(wù),主UI線程可以自由的繼續(xù)處理鍵盤和鼠標事件。
2、有效利用CPU的阻塞
多線程是有用的:當一個線程正在等待一個另一臺計算機或硬件的響應(yīng)時。當一個線程因為執(zhí)行任務(wù)而被阻塞時, 其他線程可以利用 計算機的未占用資源。
3、并行編程
何時使用多線程
??? 多線程程序一般被用來在后臺執(zhí)行耗時的任務(wù)。主線程保持運行,并且工作線程做它的后臺工作。對于Windows Forms程序來說,如果主線程試圖執(zhí)行冗長的操作,鍵盤和鼠標的操作會變的遲鈍,程序也會失去響應(yīng)。由于這個原因,應(yīng)該在工作線程中運行一個耗時任務(wù)時添加一個工作線程,即使在主線程上有一個友好的提示“處理中...”,以防止工作無法繼續(xù)。這就避免了程序出現(xiàn)由操作系統(tǒng)提示的“沒有相應(yīng)”,來誘使用戶強制結(jié)束程序的進程而導(dǎo)致錯誤。模式對話框還允許實現(xiàn)“取消”功能,允許繼續(xù)接收事件,而實際的任務(wù)已被工作線程完成。BackgroundWorker恰好可以輔助完成這一功能。
?? 在沒有用戶界面的程序里,比如說Windows Service, 多線程在當一個任務(wù)有潛在的耗時,因為它在等待另臺電腦的響應(yīng)(比如一個應(yīng)用服務(wù)器,數(shù)據(jù)庫服務(wù)器,或者一個客戶端)的實現(xiàn)特別有意義。用工作線程完成任務(wù)意味著主線程可以立即做其它的事情。
?? 另一個多線程的用途是在方法中完成一個復(fù)雜的計算工作。這個方法會在多核的電腦上運行的更快,如果工作量被多個線程分開的話(使用Environment.ProcessorCount屬性來偵測處理芯片的數(shù)量)。
?? 一個C#程序稱為多線程的可以通過2種方式:
明確地創(chuàng)建和運行多線程,或者使用.NET framework的暗中使用了多線程的特性——比如BackgroundWorker類,?線程池,threading timer,遠程服務(wù)器,或Web Services或ASP.NET程序。
第二種方式,人們別無選擇,必須使用多線程;一個單線程的ASP.NET web server不是太酷,即使有這樣的事情;幸運的是,應(yīng)用服務(wù)器中多線程是相當普遍的;唯一值得關(guān)心的是提供適當鎖機制的靜態(tài)變量問題。
? 何時不要使用多線程
??? 多線程也同樣會帶來缺點,最大的問題是它使程序變的過于復(fù)雜,擁有多線程本身并不復(fù)雜,復(fù)雜是的線程的交互作用,這帶來了無論是否交互是否是有意的,都會帶來較長的開發(fā)周期,以及帶來間歇性和非重復(fù)性的bugs。因此,要么多線程的交互設(shè)計簡單一些,要么就根本不使用多線程。除非你有強烈的重寫和調(diào)試欲望。
當用戶頻繁地分配和切換線程時,多線程會帶來增加資源和CPU的開銷。在某些情況下,太多的I/O操作是非常棘手的,當只有一個或兩個工作線程要比有眾多的線程在相同時間執(zhí)行任務(wù)塊的多。稍后我們將實現(xiàn)生產(chǎn)者/耗費者 隊列,它提供了上述功能。
2. ? ?創(chuàng)建和開始使用線程
?? 線程用Thread類來創(chuàng)建,?
一、通過ThreadStart委托來指明方法從哪里開始運行,下面是ThreadStart委托如何定義的:【也可以不用】
public?delegate?void?ThreadStart();??
調(diào)用Start方法后,線程開始運行,線程一直到它所調(diào)用的方法返回后結(jié)束。下面是一個例子,使用了C#的語法創(chuàng)建TheadStart委托:
class ThreadTest {static void Main() {Thread t = new Thread (new ThreadStart (Go));t.Start(); // Run Go() on the new thread.Go(); // Simultaneously run Go() in the main thread. }static void Go() { Console.WriteLine ("hello!"); }在這個例子中,線程t執(zhí)行Go()方法,大約與此同時主線程也調(diào)用了Go(),結(jié)果是兩個幾乎同時hello被打印出來:
二、一個線程可以僅通過指定一個方法來方便的創(chuàng)建,然后C#指出線程開始的方法(不用明確使用委托也可以,例如前面的例子)
?Thread t = new?Thread (Go);??? // No need to explicitly use ThreadStart
在這種情況,ThreadStart被編譯器自動推斷出來,
三、另一個快捷方式是使用一個lambda表達式或匿名方法:
static void Main() {Thread t = new Thread ( () => Console.WriteLine ("Hello!") );t.Start(); } 線程有一個IsAlive屬性,在調(diào)用Start()之后直到線程結(jié)束之前一直為true。一個線程一旦結(jié)束便不能重新開始了。? 將數(shù)據(jù)傳入ThreadStart中
最簡單的方法傳遞參數(shù)到一個線程:是執(zhí)行一個lambda表達式,調(diào)用該方法所需的參數(shù)
static void Main() {Thread t = new Thread ( () => Print ("Hello from t!") );t.Start(); }static void Print (string message) {Console.WriteLine (message); }使用這種方法,您可以將任意數(shù)量的參數(shù)傳遞給方法。你甚至可以把整個實現(xiàn)包在一個多語句λ表達式中
new Thread (() => {Console.WriteLine ("I'm running on another thread!");Console.WriteLine ("This is so easy!"); }).Start();使用匿名方法 同樣可以 new Thread (delegate() {... }).Start();
另一種方法是:在Thread’s?Start()方法中傳參數(shù)
static void Main() {Thread t = new Thread (Print);t.Start ("Hello from t!"); }static void Print (object messageObj) {string message = (string) messageObj; // We need to cast here Console.WriteLine (message); } 因為線程的構(gòu)造函數(shù)重載, 接受兩種委托 public delegate void ThreadStart(); public delegate void ParameterizedThreadStart (object obj);話又說回來,在上面的例子里,我們想更好地區(qū)分開每個線程的輸出結(jié)果,讓其中一個線程輸出大寫字母。我們傳入一個狀態(tài)字到Go中來完成整個任務(wù),但我們不能使用ThreadStart委托,因為它不接受參數(shù),所幸的是,.NET framework定義了另一個版本的委托叫做ParameterizedThreadStart, 它可以接收一個單獨的object類型參數(shù)(通常需要參數(shù)轉(zhuǎn)換)
Lambda表達式和捕獲變量:
正如我們所見,一個lambda表達式是最強大的方式傳遞數(shù)據(jù)到一個線程。然而,您必須小心線程開始后變量的意外修改,因為這些變量都是共享的。例如,考慮以下:
for (int i = 0; i < 10; i++)new Thread (() => Console.Write (i)).Start();輸出是非確定的! ?例如:224458891010 ?(后面的數(shù)不小于前面的數(shù),在1-10之間的數(shù),10次循環(huán)開了10個線程)
?Here’s a typical result:
The problem is that the?i?variable refers to the?same?memory location throughout the loop’s lifetime(循環(huán)周期時 兩個線程捕獲的是同一個內(nèi)存地址區(qū)). Therefore, each thread calls?Console.Write?on a variable whose value may change as it is running!
?
The solution is to use a temporary variable as follows:
for (int i = 0; i < 10; i++) {int temp = i;new Thread (() => Console.Write (temp)).Start(); } 變量temp位于每個循環(huán)迭代。因此,每個線程捕捉不同的內(nèi)存位置(主線程捕獲i的內(nèi)存區(qū),子線程捕獲temp的內(nèi)存區(qū))這樣就沒問題。。(0123456789)We can illustrate the problem in the earlier code more simply with the following example:
string text = "t1"; Thread t1 = new Thread ( () => Console.WriteLine (text) );text = "t2"; Thread t2 = new Thread ( () => Console.WriteLine (text) );t1.Start(); t2.Start();Because both lambda expressions capture the same?text?variable,?t2?is printed twice:
t2 t2? 命名線程
?線程可以通過它的Name屬性進行命名,這非常有利于調(diào)試:可以用Console.WriteLine打印出線程的名字,Microsoft Visual Studio可以將線程的名字顯示在調(diào)試工具欄的位置上。線程的名字可以在被任何時間設(shè)置,但只能設(shè)置一次,重命名會引發(fā)異常。
? 程序的主線程也可以被命名,下面例子里主線程通過CurrentThread屬性命名:
class ThreadNaming {static void Main() {Thread.CurrentThread.Name = "main";Thread worker = new Thread (Go);worker.Name = "worker";worker.Start();Go();}static void Go() {Console.WriteLine ("Hello from " + Thread.CurrentThread.Name);} }
結(jié)果也有可能相反,不能保證哪個先輸出。線程由操作系統(tǒng)來調(diào)度,每次哪個線程先運行可能不同。
? 前臺和后臺線程
?創(chuàng)建的線程默認為前臺線程(而線程池中的線程總是后臺線程),這意味著任何一個前臺線程在運行都會保持程序存活。然而后臺線程不是。一旦所有的前臺線程結(jié)束,程序也就結(jié)束了,任何后臺線程也將突然停止。。
一個線程的前臺/后臺狀態(tài) ?與他的優(yōu)先級或分配執(zhí)行時間沒有關(guān)系。。
? 改變線程從前臺到后臺不會以任何方式改變它在CPU協(xié)調(diào)程序中的優(yōu)先級和狀態(tài)。
?線程的IsBackground屬性控制它的前后臺狀態(tài),如下實例:
class PriorityTest {static void Main (string[] args) {Thread worker = new Thread (delegate() { Console.ReadLine(); });if (args.Length > 0) worker.IsBackground = true;worker.Start();} }? 1、如果程序被調(diào)用的時候沒有任何參數(shù),工作線程為前臺線程,并且將等待ReadLine語句來等待用戶按回車來觸發(fā),這期間,主線程退出,但是程序保持運行,因為一個前臺線程仍然活著。
? 2、 另一方面如果有參數(shù)傳入Main(),工作線程被賦為后臺線程,當主線程結(jié)束程序立刻退出,終止了ReadLine。后臺線程終止的這種方式,使任何最后操作都被規(guī)避了,這種方式是不太合適的。好的方式是明確等待任何后臺工作線程完成后再結(jié)束程序,有兩種方法解決:
? ? ? ? ? ? a、對創(chuàng)建的線程調(diào)用Join()方法,讓其他等待它的結(jié)束
? ? ? ? ? ? b、在線程池中的話,用一個事件等待來處理
在這兩種情況下,你應(yīng)該指定一個超時(timeout),所以可以放棄一個叛離線程(出于某種原因拒絕完成任務(wù)的線程)。
?? 擁有一個后臺工作線程是有益的,最直接的理由是它當提到結(jié)束程序它總是可能有最后的發(fā)言權(quán)。
?? 對于程序失敗退出的普遍原因就是存在“被忘記”的前臺線程。
? 線程優(yōu)先級
? 線程的Priority 屬性確定了線程相對于其它同一進程的活動的線程擁有多少執(zhí)行時間,以下是級別:
enum?ThreadPriority { Lowest, BelowNormal, Normal, AboveNormal, Highest }
只有多個線程同時為活動時,優(yōu)先級才有作用。注意:提升一個線程的優(yōu)先級之前要仔細想想—它可能導(dǎo)致其他線程的資源缺乏等問題
? 設(shè)置一個線程的優(yōu)先級為高一些,并不意味著它能執(zhí)行實時的工作,因為它受限于程序的進程的級別。要執(zhí)行實時的工作,必須提升在System.Diagnostics 命名空間下Process的級別,像下面這樣:
using (Process p = Process.GetCurrentProcess())p.PriorityClass = ProcessPriorityClass.High;ProcessPriorityClass.High 實際上是一個等級的最高優(yōu)先級:Realtime(實時)。設(shè)置進程級別到Realtime通知操作系統(tǒng):你不想讓你的進程被搶占了。如果你的程序進入一個偶然的死循環(huán),可以預(yù)期,操作系統(tǒng)被鎖住了,除了關(guān)機沒有什么可以拯救你了!基于此,High大體上被認為最好的選擇實時進程級別。
如果一個實時的程序有一個用戶界面,提升進程的級別是不太好的,因為當用戶界面UI過于復(fù)雜的時候,界面的更新耗費過多的CPU時間,拖慢了整臺電腦。
(雖然在寫這篇文章的時候,在互聯(lián)網(wǎng)電話程序Skype僥幸地這么做, 也許是因為它的界面相當簡單吧。)
?降低主線程的級別、提升進程的級別、確保實時線程不進行界面刷新,但這樣并不能避免電腦越來越慢,因為操作系統(tǒng)仍會撥出過多的CPU給整個進程。最理想的方案是使實時工作和用戶界面在不同的進程(擁有不同的優(yōu)先級)運行,通過Remoting或共享內(nèi)存方式進行通信,共享內(nèi)存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping?和?MapViewOfFile)?
? 異常處理
??任何線程創(chuàng)建范圍內(nèi)try/catch/finally塊,當線程開始執(zhí)行便不再與其有任何關(guān)系。考慮下面的程序:
public static void Main() {try {new Thread (Go).Start();}catch (Exception ex) {// 不會在這得到異常Console.WriteLine ("Exception!");}static void Go() { throw null; } } 這里try/catch語句一點用也沒有,新創(chuàng)建的線程將引發(fā)NullReferenceException異常。當你考慮到每個線程有獨立的執(zhí)行路徑的時候,便知道這行為是有道理的,補救方法是在線程處理的方法內(nèi)加入他們自己的異常處理:
?? 從.NET 2.0開始,任何線程內(nèi)的未處理的異常都將導(dǎo)致整個程序關(guān)閉,這意味著忽略異常不再是一個選項了。因此為了避免由未處理異常引起的程序崩潰,try/catch塊需要出現(xiàn)在每個線程進入的方法內(nèi),至少要在產(chǎn)品程序中應(yīng)該如此。
?
轉(zhuǎn)載于:https://www.cnblogs.com/peterYong/p/6556745.html
《新程序員》:云原生和全面數(shù)字化實踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀總結(jié)
以上是生活随笔為你收集整理的【转】C#中的线程 入门的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 集合十问
- 下一篇: c# char unsigned_dll