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