除非另有說明,否則所有的例子都假定以下命名空間被引用:
using System;
using System.Threading;
1 基本概念
在描述多線程之前,首先需要明確一些基本概念。
1.1 進程
進程指一個應用程序所運行的操作系統單元,它是操作系統環境中的基本成分、是系統進行資源分配的基本單位。它最初定義在Unix等多用戶、多任務操作系統環境下,用於表示應用程序在內存環境中執行單元的概念。
進程是執行程序的實例。當運行一個應用程序后,就生成了一個進程,這個進程擁有自己的獨立內存空間。每一個進程對應一個活動的程序,當進程激活時,操作系統就將系統的資源包括內存、I/O和CPU等分配給它,使它執行。進程在運行時創建的資源隨着進程的終止而死亡。
進程間獲得專用數據或內存的唯一途徑就是通過協議來共享內存塊,這是一種協作策略。由於進程之間的切換非常消耗資源和時間,為了提高操作系統的並發性,提高CPU的利用率,在進程下面又加入了線程的概念。
一個Process可以創建多個Thread及子Process(啟動外部程序)。
一個進程內部的線程可以共享該進程所分配的資源,線程的創建與撤銷、線程之間的切換所占用的資源比進程少很多。
1.2 線程
進程可以分為若干個獨立執行流(路徑),這些執行流被稱為線程。線程是指進程內的一個執行單元,也是進程內的可調度實體。線程是進程的一個實體,是CPU調度和分配時間的基本單位。
線程基本不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器、一組寄存器和棧),但是它可與同一進程的其它線程共享進程所擁有的全部資源。所以線程間共享內存空間很容易做到,多線程協作也很容易和便捷。
一個線程可以創建和撤銷另一個線程,同一個進程中的多個線程間可以並發執行。
線程提供了多任務處理的能力。
1.3 線程與進程的異同
地址空間:進程擁有自己獨立的內存地址空間;而線程共享進程的地址空間;換句話說就是進程間彼此是完全隔絕的,同一進程的所有線程共享(堆heap)內存;
資源擁有:進程是資源分配和擁有的單位,同一進程內的線程共享進程的資源;
系統粒度:進程是分配資源的基本單位,而線程則是系統(處理器)調度的基本單位;
執行過程:每個獨立的進程都有一個程序運行的入口、順序執行序列和程序的出口;線程不能獨立執行,必須依存於進程中;
系統開銷:創建或撤銷進程時,系統都要為之分配或回收資源(如內存空間、IO設備),進程間的切換也要消耗遠大於線程切換的開銷。
二者均可並發執行。
一個程序至少有一個進程,一個進程至少有一個線程(主線程)。主線程以函數地址的形式,如Main或WinMain函數,提供程序的啟動點,當主線程終止時,進程也隨之終止。一個進程中的所有線程都在該進程的虛擬地址空間中,使用該進程的全局變量和系統資源。
1.4 程序域
在.Net中Process由AppDomain對象所取代。
雖然AppDomain在CLR中被視為Process的替代品,但實際上AppDomain跟Process是屬於主從關系的,AppDomain被放置在一個Process中,每個Process可以擁有多個AppDomain,每個AppDomain又可擁有多個Thread對象。
Process、AppDomain、Thread的關系如下圖所示:

圖 1進程、域、線程關系圖
AppDomain定義了一些事件供程序員使用。
| 事件 |
說明 |
| AssemblyLoad |
觸發於AppDomain載入一個Assembly時 |
| DomainUnLoad |
觸發於AppDomain卸載時,也就是Unload函數被調用或是該AppDomain被消滅前 |
| ProcessExit |
當默認的AppDomain被卸載時觸發,多半是應用程序退出時 |
各AppDomain間互不影響。
1.5 並發/並行
在單CPU系統中,系統調度在某一刻只能讓一個線程運行,雖然這種調度機制有多種形式(時分/頻分),但無論如何,要通過不斷切換需要運行的線程,這種運行模式稱為並發(Concurrent)。而在多CPU系統中,可以讓兩個以上的線程同時運行,這種運行模式稱為並行(Parallel)。
1.6 異步操作
所有的程序最終都會由計算機硬件來執行,擁有DMA功能的硬件在和內存進行數據交換的時候可以不消耗CPU資源。這些無須消耗CPU時間的I/O操作是異步操作的硬件基礎。硬盤、光驅、網卡、聲卡、顯卡都具有DMA功能。
DMA(DirectMemory Access)是直接內存訪問的意思,它是不經過CPU而直接進行內存數據存儲的數據交換模式。
I/O操作包括了直接的文件、網絡的讀寫,還包括數據庫操作、Web Service、HttpRequest以及.Net Remoting等跨進程的調用。
異步操作可達到避免調用線程堵塞的目的,從而提高軟件的可響應性。
1.7 任務管理器
映射名稱列:進程並不擁有獨立於其所屬實例的映射名稱;如果運行5個Notepad拷貝,你會看到5個稱為Notepad.exe的進程;它們是根據進程ID進行區分的,該進程ID是由系統維護,並可以循環使用。
CPU列:它是進程中線程所占用的CPU時間百分比
每個任務管理器中的進程,其實內部都包含若干個線程,每個時間點都是某個程序進程中的某個線程在運行。
2 多線程基礎
2.1 為什么要使用多線程
Ø 並發需要
在C/S或B/S模式下的服務端需要處理來自不同終端的並發請求,使用單線程是不可思議的。
Ø 提高應用程序的響應速度
當一個耗時的操作進行時,當前程序都會等待這個操作結束,此時程序不會響應鍵盤、鼠標、菜單等操作,程序處於假死狀態;使用多線程可將耗時長的操作(Time Consuming)置於一個新的線程,此時程序仍能響應用戶的其它操作。
Ø 提高CPU利用率
在多CPU體系中,操作系統會保證當線程數不大於CPU數目時,不同的線程運行於不同的CPU上。
Ø 改善程序結構
一個既長又復雜的進程可以考慮分為多個線程,成為幾個獨立或半獨立的運行部分,這樣的程序會利於理解和修改。
Ø 花銷小、切換快
線程間的切換時間很小,可以忽略不計
Ø 方便的通信機制
線程間共享內存,互相間交換數據很簡單。
多線程的意義在於一個應用程序中,有多個執行部分可以同時執行:一個線程可以在后台讀取數據,而另一個線程可以在前台展現已讀取的數據。
C#支持通過多線程並行地執行代碼,一個線程有它獨立的執行路徑,能夠與其它的線程同時地運行。一個C#程序開始於一個單線程,這個單線程是被CLR和操作系統(也稱為“主線程”)自動創建的,並具有多線程創建額外的線程。
2.2 何時使用多線程
多線程程序一般被用來在后台執行耗時的任務:主線程保持運行,而工作線程執行后台工作。
對於Windows Forms程序來說,如果主線程執行了一個冗長的操作,鍵盤和鼠標的操作會變的遲鈍,程序也會失去響應,進入假死的狀態,可能導致用戶強制結束程序進程而出現錯誤。有鑒於此,應該在主線程運行一個耗時任務時另外添加一個工作線程,同時在主線程上有一個友好的提示“處理中...”,允許繼續接收事件(比如響應鼠標、鍵盤操作)。同時程序還應該實現“取消”功能,允許取消/結束當前工作線程。BackgroundWorker類就提供這一功能。
在沒有用戶界面的程序里,比如說WindowsService中使用多線程特別的有意義。當一個任務有潛在的耗時(在等待被請求方的響應——比如應用服務器,數據庫服務器),用工作線程完成任務意味着主線程可以在發送請求后立即做其它的事情。
另一個多線程的用途是在需要完成一個復雜的計算工作時。它會在多核的電腦上運行得更快,如果工作量被多個線程分開的話(C#中可使用Environment.ProcessorCount屬性來偵測處理芯片的數量)。
一個C#程序成為多線程可以通過2種方式來實現:明確地創建和運行多線程,或者使用.NET Framework中封裝了多線程的類——比如BackgroundWorker類。
線程池,Threading Timer,遠程服務器,或WebServices或ASP.NET程序將別無選擇,必須使用多線程;一個單線程的ASP.Net Web Service是不可想象的;幸運的是,應用服務器中多線程是相當普遍的;唯一值得關心的是提供適當鎖機制的靜態變量問題。
2.3 何時不用多線程
多線程也同樣會帶來缺點,最大的問題是它使程序變的過於復雜,擁有多線程本身並不復雜,復雜是的線程的交互作用,無論交互是否是有意的,都會帶來較長的開發周期,以及帶來間歇性和非重復性的Bugs。因此,要么多線程的交互設計簡單一些,要么就根本不使用多線程,除非你有強烈的重寫和調試欲望。
當用戶頻繁地分配和切換線程時,多線程會帶來增加資源和CPU的開銷。在某些情況下,太多的I/O操作是非常棘手的,當只有一個或兩個工作線程要比有眾多的線程在相同時間執行任務快的多。
2.4 創建和開始使用多線程
線程可以使用Thread類來創建,通過ThreadStart委托來指明方法從哪里開始運行,下面是ThreadStart委托定義:
| public delegate void ThreadStart(); |
調用Start方法后,線程開始運行,線程一直到它所調用的方法返回后結束。下面是一個例子,使用了C#的語法創建TheadStart委托:
| Class ThreadTest { static void Main() { Thread t = new Thread(new ThreadStart(Go)); t.Start(); // 在新線程中運行Go() Go(); // 同時在主線程中運行Go() } static void Go() { Console.WriteLine ("hello!"); } |
在這個例子中,線程t執行Go()方法,大約與此同時主線程也調用了Go(),結果是兩個幾乎同時hello被打印出來:
hello!
hello!
線程有一個IsAlive屬性,在調用Start()之后直到線程結束之前一直為true。
一個線程一旦結束便不能重新開始,只能重新創建一個新的線程。
2.5 帶參數啟動線程
在上面的例子里,我們想更好地區分開每個線程的輸出結果,讓其中一個線程輸出大寫字母。我們傳入一個狀態字到Go中來完成整個任務,但不能使用ThreadStart委托,因為它不接受參數。
2.5.1 ParameterizedThreadStart
.NET framework定義了另一個版本的委托叫做ParameterizedThreadStart,它可以接收一個單獨的object類型參數:
| public delegate void ParameterizedThreadStart(object obj); |
之前的例子看起來是這樣的:
| Class ThreadTest { static void Main() { Thread t = new Thread(Go); t.Start (true); // == Go (true) Go (false); } static void Go (object upperCase) { bool upper = (bool) upperCase; Console.WriteLine (upper ? "HELLO!" : "hello!"); }
|
輸出結果:
hello!
HELLO!
在整個例子中,編譯器自動推斷出ParameterizedThreadStart委托,因為Go方法接收一個單獨的object參數,就像這樣寫:
| Thread t = new Thread(new ParameterizedThreadStart(Go)); t.Start (true); |
ParameterizedThreadStart的特性是在使用之前我們必需對我們想要的類型(這里是bool)進行裝箱操作,並且它只能接收一個參數。
2.5.2 匿名方法
需要接收多個參數的解決方案是使用一個匿名方法調用,方法如下:
| static void Main() { Thread t = new Thread(delegate() { WriteText ("Hello"); }); t.Start(); } static void WriteText (stringtext) { Console.WriteLine (text); } |
它的優點是目標方法(這里是WriteText),可以接收任意數量的參數,並且沒有裝箱操作。不過這需要將一個外部變量放入到匿名方法中,如下示例:
| static voidMain() { stringtext = "Before"; Threadt = new Thread(delegate() { WriteText (text); }); text = "After"; t.Start(); } static void WriteText (stringtext) { Console.WriteLine (text); } |
需要注意的是,當外部變量的值被修改,匿名方法可能進行無意的互動,導致一些古怪的現象。一旦線程開始運行,外部變量最好被處理成只讀的——除非有人願意使用適當的鎖。
2.5.3 對象實例方法
另一種較常見的方式是將對象實例的方法而不是靜態方法傳入到線程中,對象實例的屬性可以告訴線程要做什么,如下列重寫了原來的例子:
| Class ThreadTest { Bool upper; static void Main() { ThreadTest instance1 = new ThreadTest(); instance1.upper = true; Thread t = new Thread(instance1.Go); t.Start(); ThreadTest instance2 = new ThreadTest(); instance2.Go(); // 主線程——運行 upper=false } Void Go() { Console.WriteLine (upper ? "HELLO!" : "hello!"); } |
2.6 命名線程
線程可以通過它的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); } } |
輸出
Hellofrom main
Hellofrom worker

圖 2 .Net框架中監控線程
上圖為.Net框架中監控當前線程,可通過名稱找到某個線程,查看它的執行情況。
2.7 前台和后台線程
線程分為兩種:用戶界面線程(前台線程)和工作線程(后台線程)。
用戶界面線程通常用來處理用戶的輸入並響應各種事件和消息;工作線程用來執行程序的后台處理任務,比如計算、調度、對串口的讀寫操作等。
線程默認為前台線程,這意味着任何前台線程在運行都會保持程序存活。C#也支持后台線程,當所有前台線程結束后,它們不維持程序的存活。
改變線程從前台到后台不會以任何方式改變它在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(); } } |
如果程序被調用的時候沒有任何參數,工作線程為前台線程,並且將等待ReadLine語句來等待用戶的觸發回車,這期間,主線程退出,但是程序保持運行,因為一個前台線程仍然活着。
另一方面如果有參數傳入Main(),工作線程被賦值為后台線程,當主線程結束程序立刻退出,終止了ReadLine。
后台線程終止的這種方式,使任何最后操作都被規避了,這種方式是不太合適的。好的方式是明確等待任何后台工作線程完成后再結束程序,可能用一個timeout(大多用Thread.Join)。如果因為某種原因某個工作線程無法完成,可以用試圖終止它的方式,如果失敗了,再拋棄線程,允許它與進程一起消亡。
擁有一個后台工作線程是有益的,最直接的理由是當提到結束程序它總是可能有最后的發言權。交織以不會消亡的前台線程,保證程序的正常退出。拋棄一個前台工作線程是尤為險惡的,尤其對Windows Forms程序,因為程序直到主線程結束時才退出(至少對用戶來說),但是它的進程仍然運行着。在Windows任務管理器它將從應用程序欄消失不見,但卻可以在進程欄找到它。除非用戶找到並結束它,它將繼續消耗資源,並可能阻止一個新的實例的運行從開始或影響它的特性。
對於程序失敗退出的普遍原因就是存在“被忘記”的前台線程。
| 線程類型 |
動作 |
結束 |
后續處理 |
| 前台線程 |
主程序關閉 |
否 |
顯示關閉線程/殺掉當前進程 |
| 后台線程 |
主程序關閉 |
是 |
無 |
2.8 線程優先級
線程的Priority 屬性確定了線程相對於其它同一進程的活動的線程擁有多少執行時間,以下是級別:
| enum ThreadPriority{ Lowest, BelowNormal, Normal, AboveNormal, Highest } |
只有多個線程同時為活動時,優先級才有作用。
設置一個線程的優先級為高一些,並不意味着它能執行實時的工作,因為它受限於程序的進程的級別。要執行實時的工作,必須提升在System.Diagnostics 命名空間下Process的級別,像下面這樣:
| Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High; |
ProcessPriorityClass.High其實是一個短暫缺口的過程中的最高優先級別:Realtime。設置進程級別到Realtime通知操作系統:你不想讓你的進程被搶占了。如果你的程序進入一個偶然的死循環,可以預期,操作系統被鎖住了,除了關機沒有什么可以拯救你了!基於此,High大體上被認為最高的有用進程級別。
如果一個實時的程序有一個用戶界面,提升進程的級別是不太好的,因為當用戶界面UI過於復雜的時候,界面的更新耗費過多的CPU時間,拖慢了整台電腦。(雖然在寫這篇文章的時候,在互聯網電話程序Skype僥幸地這么做, 也許是因為它的界面相當簡單吧。)降低主線程的級別、提升進程的級別、確保實時線程不進行界面刷新,但這樣並不能避免電腦越來越慢,因為操作系統仍會撥出過多的CPU給整個進程。最理想的方案是使實時工作和用戶界面在不同的進程(擁有不同的優先級)運行,通過Remoting或共享內存方式進行通信,共享內存需要Win32 API中的 P/Invoking。(可以搜索看看CreateFileMapping 和MapViewOfFile)
2.9 線程異常處理機制
任何線程在創建時使用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異常,而主線程無法捕獲到。這是因為每個線程都有獨立的執行路徑。最好的補救方法是在線程處理的方法內加入異常處理:
| public static void Main() { new Thread(Go).Start(); } static void Go() { try{ ... throw null; // 這個異常會被捕捉到 ... } catch(Exceptionex) { 記錄異常日志,或通知另一個線程錯誤發生 ... } |
從.NET 2.0開始,任何線程內未處理的異常都將導致整個程序關閉,這意味着忽略線程異常將是一個災難。
為了避免由未處理異常引起的程序崩潰,try/catch語句塊需要出現在每個線程具體實現的方法內。對於經常使用“全局”異常處理的Windows Forms程序員來說,這將很不習慣,就像下面這樣的代碼:
| Using System; Using System.Threading; Using System.Windows.Forms; static class Program{ static void Main() { Application.ThreadException += HandleError; Application.Run (new MainForm()); } static void HandleError (object sender, ThreadExceptionEventArgs e) { 記錄異常或者退出程序或者繼續運行... } } |
Application.ThreadException事件在異常被拋出時觸發,以一個Windows信息(比如:鍵盤,鼠標或者 "paint" 等信息)的方式。簡而言之,覆蓋了一個Windows Forms程序的幾乎所有代碼異常。這使開發者產生一種虛假的安全感——所有的異常都被主線程異常處理機制捕獲。但實際的情況卻是,由工作線程拋出的異常不會被Application.ThreadException捕捉到。
.NET framework提供了一個更低級別的異常捕獲事件:AppDomain.UnhandledException,這個事件在任何類型的程序(有或沒有用戶界面)中的任何線程有任何未處理的異常拋出時被觸發。盡管它提供了比較完善的異常處理解決機制,但是這並不意味着程序不會崩潰,也不意味着能取消.NET異常對話框。
| 在產品程序中,明確地使用異常處理在所有線程進入的方法中是必要的,可以使用包裝類和幫助類來分解工作來完成任務,比如使用BackgroundWorker類(在第三部分進行討論) |
2.10 線程是如何工作的
線程被一個線程協調程序管理着——一個CLR委托給操作系統的函數。線程協調程序確保將所有活動的線程被分配適當的執行時間;並且那些等待或阻止的線程——比如說在排它鎖中、或在用戶輸入——都是不消耗CPU時間的。
在單核處理器的電腦中,線程協調程序完成一個時間片之后迅速地在活動的線程之間進行切換執行。這就導致“波濤洶涌”的行為,例如在第一個例子,每次重復的X 或 Y 塊相當於分給線程的時間片。在Windows XP中時間片通常在10毫秒內選擇要比CPU開銷在處理線程切換的時候的消耗大的多。(即通常在幾微秒區間)
在多核的電腦中,多線程被實現成混合時間片和真實的並發——不同的線程在不同的CPU上運行。但這仍然會出現一些時間切片,因為操作系統的服務線程、以及一些其他的應用程序都會爭奪對CPU的使用權。
線程由於外部因素(比如時間片)被中斷被稱為被搶占,在大多數情況下,一個線程在被搶占的那一刻就失去了對它的控制權。
2.11 線程安全
當使用線程(Thread)時,程序員必須注意同步處理的問題,理論上每個Thread都是獨立運行的個體,由CLR來主導排程,視Thread的優先權的設置,每個Thread會分到特定的運行時間,當某個Thread的運行時間用完時,CLR就會強制將運行權由該Thread取回,轉交給下個Thread,這也就意味着Thread本身無法得知自己何時會喪失運行權,所以會發生所謂的race condition(競速狀態)。
當兩個線程爭奪一個鎖的時候(在這個例子里是locker),一個線程等待,或者說被阻止到那個鎖變的可用。在這種情況下,就確保了在同一時刻只有一個線程能進入臨界區,所以"Done"只被打印了1次。代碼以如此方式在不確定的多線程環境中被叫做線程安全。
臨時暫停,或阻止是多線程的協同工作,同步活動的本質特征。等待一個排它鎖被釋放是一個線程被阻止的原因,另一個原因是線程想要暫停或Sleep一段時間:
| Thread.Sleep (TimeSpan.FromSeconds (30)); // 阻止30秒 |
一個線程也可以使用它的Join方法來等待另一個線程結束:
| Threadt = new Thread(Go); // 假設Go是某個靜態方法 t.Start(); t.Join(); // 等待(阻止)直到線程t結束 |
2.12 異步模式對比
線程不是一個計算機的硬件功能,而是操作系統提供的一種邏輯功能,線程本質上是進程中一段並發運行的代碼,所以線程需要操作系統投入CPU資源來運行和調度。
異步模式無須額外的線程負擔,並且使用回調的方式進行處理,在設計良好的情況下,處理函數可以不必共享變量,減少了死鎖的可能。不過,編寫異步操作的復雜程度比較高,程序主要使用回調方式進行處理,與人的思維方式有出入,而且難以調試。
計算密集型工作使用多線程(如圖形處理、算法);IO密集型工作使用異步機制。
| /// <summary> /// 異步調用返回事件 /// </summary> public event OnAsynCallBack OnCallBack = null;
/// <summary> /// 構造函數,用於異步CallBiz /// </summary> /// <param name="onCallBack">異步調用返回事件</param> /// <param name="callId">調用ID</param> /// <param name="timeOutMs">超時(毫秒)</param> /// <param name="bizName">業務名稱</param> /// <param name="funName">方法名稱</param> /// <param name="argsJson">參數JSON數組</param> public AsynCall(OnAsynCallBack onCallBack, string callId, int timeOutMs,string bizName, string funName,params string[] argsJson) { TimeCall = TimeHelper.GetTicks(); TimeBack = -1; TimeUse = -1;
OnCallBack = onCallBack; ReturnType = null; CallId = callId; TimeOutMs = timeOutMs;
BizName = bizName; FunName = funName; ArgsJson = argsJson;
this.InFiles = OtCom.Thread_ClsInFiles(); } |
轉載:http://blog.csdn.net/36/article/details/50541971
