從此圖中我們會發現 .NET 與C# 的每個版本發布都是有一個“主題”。即:C#1.0托管代碼→C#2.0泛型→C#3.0LINQ→C#4.0動態語言→C#5.0異步編程。現在我為最新版本的“異步編程”主題寫系列分享,期待你的查看及點評。
開始:《異步編程:線程概述及使用》
做交互式客戶端應用程序,用戶總希望程序能時刻響應UI操作;做高性能服務器開發,使用者總希望服務器能同時處理多個請求……等等,這時我們可以使用多線程技術來保證UI線程可響應、提高服務器吞吐量、提升程序處理速度,設置任務優先級進行調度……
多線程技術只是多個線程在操作系統分配的不同時間片里執行,並不是程序開12個線程12個線程都在同一個 “時間點”執行,同一“時間點”能執行多少線程由CPU決定,各個執行線程的銜接由操作系統進行調度。即,在線程數量超出用於處理它們的處理器數量的情況下,操作系統將定期為每個線程調度一個時間片來控制處理器,以此來模擬同時並發。
在認識線程前,我們需要了解下CPU,了解下進程。
多核心CPU超線程CPU
1. 多核心處理器(CPU)
指在一塊處理器(CPU)中含有多個處理單元,每一個處理單元它就相當於一個單核處理器(CPU)。因此,多核處理器的功能就相當於多台單核處理器電腦聯機作戰。
2. 超線程處理器(CPU)
指在一塊CPU中,用虛擬的方法將一個物理核心模擬成多個核心(一般情況是一個單物理核心,模擬成二個核心,也即所謂的二線程。只有當線程數比物理核心數多才能叫超線程。如四核四線程並不是超線程,而四核八線程才能叫超線程)。
3. 優缺點:
1) 多核心是真正的物理核心,一塊多核心的處理器(CPU),就相當於多塊單核心的處理器(CPU)相互協作。因此,從理論上說,多核心比超線程具有更高運算能力。雖然多核心比超線程的運算速度快很多,但多核心也有一個明顯的缺點,那就是多核心的使用效率比超線程處理器(CPU)低。因為,多核心在處理數據時,它們相互“合作”的並不是很完美,常常某個核心需要等待其他核心的計算數據,從而耽誤時間,被迫怠工。另外,由於目前多核心都是采用共享緩存,這更使多核心的CPU運算速度減慢不少(因為:CPU讀取Cache時是以行為單位讀取的,如果兩個硬件線程的兩塊不同內存位於同一Cache行里,那么當兩個硬件線程同時在對各自的內存進行寫操作時,將會造成兩個硬件線程寫同一Cache行的問題,它會引起競爭)。
2) 超線程是用虛擬的方法將一個物理核心虛擬成多個核心,它能夠最大限度地利用現有的核心資源,具有較高性價比。
操作系統對多核處理器的支持
主要體現在調度和中斷上:
1. 對任務的分配進行優化。使同一應用程序的任務盡量在同一個核上執行。
2. 對任務的共享數據優化。由於多核處理器(Chip Multi-Processor,CMP)體系結構共享緩存(目前),可以考慮改變任務在內存中的數據分布,使任務在執行時盡量增加緩存的命中率。
3. 對任務的負載均衡優化。當任務在調度時,出現了負載不均衡,考慮將較忙處理器中與其他任務最不相關的任務遷移,以達到數據的沖突最小。
4. 支持搶先多任務處理的操作系統可以創建多個進程中的多個線程同時執行的效果。它通過以下方式實現這一點:在需要處理器時間的線程之間分割可用處理器時間,並輪流為每個線程分配處理器時間片。當前執行的線程在其時間片結束時被掛起,而另一個線程繼續運行。當系統從一個線程切換到另一個線程時,它將保存被搶先的線程的線程上下文,並重新加載線程隊列中下一個線程的已保存線程上下文。
進程和線程
1. 進程
進程是應用程序的執行實例,每個進程是由私有的虛擬地址空間、代碼、數據和其它各種系統資源組成,進程在運行過程中創建的資源隨着進程的終止而被銷毀,所使用的系統資源在進程終止時被釋放或關閉。
2. 線程
線程是進程內部的一個執行單元。系統創建好進程后,實際上就啟動執行了該進程的主執行線程。主執行線程終止了,進程也就隨之終止。
每個線程都維護異常處理程序、調度優先級和線程上下文。(線程上下文,當前執行的線程在其時間片結束時被掛起,而另一個線程繼續運行。當系統從一個線程切換到另一個線程時,它將保存被搶先的線程的線程上下文,並重新加載線程隊列中下一個線程的已保存線程上下文)
3. 關系
操作系統使用進程將它們正在執行的不同應用程序分開,.NET Framework 將操作系統進程進一步細分為System.AppDomain (應用程序域)的輕量托管子進程。
線程是CPU的調度單元,是進程中的執行單位,一個進程中可以有多個線程同時執行代碼。
操作系統中,CPU的兩種競爭策略
操作系統中,CPU競爭有很多種策略。Unix系統使用的是時間片算法,而Windows則屬於搶占式的。
1. 在時間片算法中,所有的進程排成一個隊列。操作系統按照他們的順序,給每個進程分配一段時間,即該進程允許運行的時間。如果在時間片結束時進程還在運行,則CPU將被剝奪並分配給另一個進程。如果進程在時間片結束前阻塞或結束,則CPU當即進行切換。調度程序所要做的就是維護一張就緒進程列表,當進程用完它的時間片后,它被移到隊列的末尾。
2. 所謂搶占式操作系統,就是說如果一個進程得到了 CPU 時間,除非它自己放棄使用 CPU ,否則將完全霸占 CPU 。因此可以看出,在搶占式操作系統中,操作系統假設所有的進程都是“人品很好”的,會主動退出 CPU 。在搶占式操作系統中,假設有若干進程,操作系統會根據他們的優先級、飢餓時間(已經多長時間沒有使用過 CPU 了),給他們算出一個總的優先級來。操作系統就會把 CPU 交給總優先級最高的這個進程。當進程執行完畢或者自己主動掛起后,操作系統就會重新計算一次所有進程的總優先級,然后再挑一個優先級最高的把 CPU 控制權交給他。
線程Thread類詳解
靜態屬性 |
CurrentThread ,CurrentContext,CurrentPrincipal(負責人) |
靜態方法 |
AllocateDataSlot(),AllocateNamedDataSlot(),FreeNamedDataSlot(),GetNamedDataSlot(),GetData(),SetData(),BeginCriticalRegion()[關鍵的],EndCriticalRegion(),BeginThreadAffinity(),EndThreadAffinity(), GetDomain(),GetDomainID(), ResetAbort(),Sleep(),SpinWait(),MemoryBarrier(),VolatileRead(),VolatileWrite(),Yield() |
實例屬性 |
Priority,ThreadState ,IsAlive,IsBackground,IsThreadPoolThread,ManagedThreadId,ApartmentState,CurrentCulture,CurrentUICulture,ExecutionContext,Name |
實例方法 |
GetHashCode(),Start(),Abort(), Resume(),Suspend(),Join(),Interrupt(),GetApartmentState(),SetApartmentState(),TrySetApartmentState(),GetCompressedStack(),SetCompressedStack(),DisableComObjectEagerCleanup() |
1) CurrentContext 獲取線程正在其中執行的當前上下文。主要用於線程內部存儲數據。
2) ExecutionContext 獲取一個System.Threading.ExecutionContext對象,該對象包含有關當前線程的各種上下文的信息。主要用於線程間數據共享。
3) IsThreadPoolThread 獲取一個值,該值指示線程是否屬於托管線程池。
4) ManagedThreadId 獲取一個整數,表示此托管線程的唯一標識符。
5) IsBackground 獲取或設置一個值,該值指示某個線程是否為后台線程。
前台線程和后台線程並不等同於主線程和工作線程,如果所有的前台線程終止,那所有的后台線程也會被自動終止。應用程序必須運行完所有的前台線程才可以退出,所以,要特別注意前台線程的使用,會造成應用程序終止不了。
默認情況下:通過Thread.Start()方法開啟的線程都默認為前台線程。可以設置IsBackground屬性將線程配置為后台線程。
屬於托管線程池的線程(即其 IsThreadPoolThread 屬性為 true 的線程)是后台線程。從非托管代碼進入托管執行環境的所有線程都被標記為后台線程。
6) IsAlive 判斷此線程是否還存活。經測試只有 Unstarted、Stopped 返回false;其他線程狀態都返回true。
2. 創建線程
public Thread(ParameterizedThreadStart start); public Thread(ThreadStart start); public Thread(ParameterizedThreadStart start, int maxStackSize); public Thread(ThreadStart start, int maxStackSize);
Thread包含使用ThreadStart或ParameterizedThreadStart委托做參數的構造函數,這些委托包裝調用Start()時由新線程執行的方法。
線程一旦啟動,就不必保留對Thread對象的引用。線程會繼續執行直到線程所調用委托執行完畢。
1) 向線程傳遞數據(見示例)
我們可以直接使用接收ParameterizedThreadStart參數Thread構造函數創建新線程,再通過Start(object parameter)傳入參數並啟動線程。由於Start方法接收任何對象,所以這並不是一種類型安全的實現。
所以我們可以使用一種替代方案:將線程執行的方法和待傳遞數據封裝在幫助器類中,使用無參的Start()啟動線程。必要的時候需在幫助器類中使用同步基元對象避免線程共享數據的死鎖和資源爭用。
Thread構造函數接收的ThreadStart或ParameterizedThreadStart委托參數,這兩個委托的聲明都是返回void,即線程執行完后不會有數據返回(實際上主線程也不會等待Thread創建的新線程返回,否則創建新線程就無意義了)。那么如何在異步執行完時做出響應呢?使用回調方法。
示例----關鍵代碼(詳見Simple4CallBackWithParam()):
// 包裝異步方法的委托 public delegate void ExampleCallback(int lineCount); // 幫助器類 public class ThreadWithState { private string boilerplate; private int value; private ExampleCallback callback; public ThreadWithState(string text, int number, ExampleCallback callbackDelegate) { boilerplate = text; value = number; callback = callbackDelegate; } public void ThreadProc() { Console.WriteLine(boilerplate, value); // 異步執行完時調用回調 if (callback != null) callback(1); } } // 異步調用 // 將需傳遞給異步執行方法數據及委托傳遞給幫助器類 ThreadWithState tws = new ThreadWithState( "This report displays the number {0}.", 42, new ExampleCallback(ResultCallback) ); Thread t = new Thread(new ThreadStart(tws.ThreadProc)); t.Start();
3. 調度線程
使用Thread.Priority屬性獲取或設置任何線程的優先級。優先級:Lowest <BelowNormal< Normal <AboveNormal< Highest
public enum ThreadPriority { Lowest = 0, BelowNormal = 1, // 默認情況下,線程具有 Normal 優先級。 Normal = 2, AboveNormal = 3, Highest = 4, }
每個線程都具有分配給它的線程優先級。在公共語言運行庫中創建的線程最初分配的優先級為ThreadPriority.Normal。在運行庫外創建的線程會保留它們在進入托管環境之前所具有的優先級。
線程是根據其優先級而調度執行的。所有線程都是由操作系統分配處理器時間片的,如果具有相同優先級的多個線程都可用,則計划程序將遍歷處於該優先級的線程,並為每個線程提供一個“固定的時間片”來執行,執行完“固定的時間片”后就切換線程,若當前任務還未執行完,則必須等待下一次的調度。
低優先級的線程並不是被阻塞直到較高優先級的線程完成,低優先級的線程只是在相同時間間隔被CPU調度的次數相對較少。
重要提示:
最好是降低一個線程的優先級,而不是提升另一個線程的優先級。如果線程要執行一個長時間運行的計算限制任務,比如編譯代碼、拼寫檢查、電子表格重新計算等,一般應降低該線程的優先級。如果線程要快速響應某個事件,然后運行非常短暫的時間,再恢復為等待狀態,則應提高該線程的優先級。高優先級線程在其生命中的大多數時間里都應處於等待狀態,這樣才不至於影響系統的總體響應能力。
4. 線程狀態
Thread.ThreadState屬性提供一個位掩碼,用它指示線程的當前狀態。
[Flags] public enum ThreadState { //線程已啟動,它未被阻塞,並且沒有掛起的 ThreadAbortException。 Running = 0, // 正在請求線程停止。 這僅用於內部。 StopRequested = 1, // 正在請求線程掛起。 SuspendRequested = 2, // 線程正作為后台線程執行(相對於前台線程而言)。 此狀態可以通過設置 Thread.IsBackground 屬性來控制。 Background = 4, // 尚未對線程調用 Thread.Start() 方法。 Unstarted = 8, // 線程已停止。 Stopped = 16, // 線程已被阻止。 這可能是因為:調用 Thread.Sleep(System.Int32) 或 Thread.Join()、請求鎖定(例如通過調用Monitor.Enter(System.Object) 或 Monitor.Wait(System.Object,System.Int32,System.Boolean))或等待線程同步對象(例如Threading.ManualResetEvent)。 WaitSleepJoin = 32, // 線程已掛起。 Suspended = 64, // 已對線程調用了 Thread.Abort(System.Object) 方法,但線程尚未收到試圖終止它的掛起的ThreadAbortException。 AbortRequested = 128, // 線程狀態包括 ThreadState.AbortRequested 並且該線程現在已死,但其狀態尚未更改為 ThreadState.Stopped。 Aborted = 256, }
由於 Running 狀態的值為 0 (枚舉的默認值),因此不可能執行位測試來發現此狀態。但可以使用此測試(以偽代碼表示):if ((state & (Unstarted | Stopped)) == 0){}
線程可以同時處於多個狀態中。例如,如果某個線程在 Monitor.Wait 調用被阻止,並且另一個線程對同一個線程調用 Abort,則該線程將同時處於 WaitSleepJoin 和 AbortRequested 狀態。在這種情況下,一旦該線程從對 Wait 的調用返回或該線程中斷,它就會收到 ThreadAbortException。
5. 線程狀態操作方法
操作:Start(),Abort(),Suspend(),Resume(), Join(),Interrupt()以及靜態方法Sleep()和ResetAbort()
線程操作與線程狀態對應的表和圖如下:
操作 |
所得到的新狀態 |
調用 Thread 類的構造函數。 |
Unstarted |
另一個線程調用 Thread.Start。 |
Unstarted |
線程響應 Thread.Start 並開始運行。 |
Running |
線程調用 Thread.Sleep。 |
WaitSleepJoin |
線程對另一個對象調用 Monitor.Wait。 |
|
線程對另一個線程調用 Thread.Join。 |
|
另一個線程調用 Thread.Suspend。 |
SuspendRequested |
線程返回到托管代碼時,線程響應 Thread.Suspend 請求。 |
Suspended |
另一個線程調用 Thread.Resume。 |
Running |
另一個線程調用 Thread.Abort。 |
AbortRequested |
線程返回到托管代碼時,線程響應 Thread.Abort。 |
Aborted ,然后 Stopped |
1) 開始線程
調用Start()開始一個線程。一旦線程由於調用 Start 而離開 Unstarted 狀態,那么它將無法再返回到 Unstarted 狀態(最后被銷毀)。
2) 線程銷毀及取消銷毀
調用線程的Abort()實例方法可以銷毀目標線程實例,調用Thread.ResetAbort() 來取消線程銷毀。()
請注意:
a) 異常是在目標線程捕獲,而不是主線程的try-catch-finally。
b) 是“可以”銷毀目標線程實例,不能保證線程會結束。因為
l 目標線程可捕捉 ThreadAbortException 異常並在此catch塊中調用Thread.ResetAbort() 來取消線程銷毀,取消后try塊外面的代碼可正常運行。
l 在finally塊中可以執行任意數量的代碼(在finally中調用Thread.ResetAbort()不能取消線程的銷毀),若不給予超時設置也無法保證線程會結束。
c) 注意Abort()后要在catch或finally中清理對象。
d) 如果您希望一直等到被終止的線程結束,可以調用Thread.Join()方法。Join 是一個模塊化調用,它直到線程實際停止執行時才返回。
e) 如果調用線程的 Abort 方法時線程正在執行非托管代碼,則運行庫將其標記為ThreadState.AbortRequested。待線程返回到托管代碼時引發ThreadAbortException異常。
f) 一旦線程被中止ThreadState.Stoped,它將無法重新啟動。
示例----關鍵代碼(詳見Simple4Abort())
Thread t = new Thread( () => { try { Console.WriteLine("try內部,調用Abort前。"); // ……等待其他線程調用該線程的Abort() Console.WriteLine("try內部,調用Abort后。"); } catch (ThreadAbortException abortEx) { Console.WriteLine("catch:" + abortEx.GetType()); Thread.ResetAbort(); Console.WriteLine("catch:調用ResetAbort()。"); } catch (Exception ex) { Console.WriteLine("catch:" + ex.GetType()); } finally { Console.WriteLine("finally"); // 在finally中調用Thread.ResetAbort()不能取消線程的銷毀 //Thread.ResetAbort(); //Console.WriteLine("調用ResetAbort()。"); } Console.WriteLine("try外面,調用Abort后(若再catch中調用了ResetAbort,則try塊外面的代碼依舊執行,即:線程沒有終止)。"); } // 其他線程調用該線程的Abort() t.Abort(); Console.WriteLine("主線程,調用Abort。");
輸出:
若在catch中沒有調用Thread.ResetAbort(),哪么try塊外面的代碼就不會輸出(詳見輸出截圖的兩處紅線)。
3) 阻塞線程
調用Sleep()方法使當前線程放棄剩余時間片,立即掛起(阻塞)並且在指定時間內不被調度。
Sleep(timeout),會有條件地將調用線程從當前處理器上移除,並且有可能將它從線程調度器的可運行隊列中移除。這個條件取決於調用 Sleep 時timeout 參數。
a) 當 timeout = 0, 即 Sleep(0),如果線程調度器的可運行隊列中有大於或等於當前線程優先級的就緒線程存在,操作系統會將當前線程從處理器上移除,調度其他優先級高的就緒線程運行;如果可運行隊列中的沒有就緒線程或所有就緒線程的優先級均低於當前線程優先級,那么當前線程會繼續執行,就像沒有調用 Sleep(0)一樣。一個時間片結束時,如果Windows決定再次調度同一個線程(而不是切換到另一個線程),那么Windows不會執行上下文切換。
b) 當 timeout > 0 時,如:Sleep(1),可能會引發線程上下文切換(如果發生線程切換):調用線程會從線程調度器的可運行隊列中被移除一段時間,這個時間段約等於 timeout 所指定的時間長度。為什么說約等於呢?是因為睡眠時間單位為毫秒,這與系統的時間精度有關。通常情況下,系統的時間精度為 10 ms,那么指定任意少於 10 ms但大於 0 ms 的睡眠時間,均會向上求值為 10 ms。
調用Thread.Sleep(Timeout.Infinite)將使線程休眠,直到其他運行線程調用 Interrupt ()中斷處於WaitSleepJoin線程狀態的線程,或調用Abort()中止線程。
應用實例:輪詢休眠
while (!proceed) Thread.Sleep (x); // "輪詢休眠!"
4) 線程的掛起和喚醒
可結合Suspend()與Resume()來掛起和喚醒線程,這兩方法已過時。
當對某線程調用Suspend()時,系統會讓該線程執行到一個安全點,然后才實際掛起該線程(與Thread.Sleep()不同, Suspend()不會導致線程立即停止執行)。無論調用了多少次 Suspend(),調用Resume()均會使另一個線程脫離掛起狀態,並導致該線程繼續執行。
注意:由於Suspend()和Resume()不依賴於受控制線程的協作,因此,它們極具侵犯性並且會導致嚴重的應用程序問題,如死鎖(例如,如果您在安全權限評估期間掛起持有鎖的線程,則AppDomain中的其他線程可能被阻止。如果您在線程正在執行類構造函數時掛起它,則AppDomain中試圖使用該類的其他線程將被阻止。很容易發生死鎖)。
線程的安全點:
是線程執行過程中可執行垃圾回收的一個點。垃圾回收器在執行垃圾回收時,運行庫必須掛起除正在執行回收的線程以外的所有線程。每個線程在可以掛起之前都必須置於安全點。
5) Join()
在線程A中調用線程B的Join()實例方法。在繼續執行標准的 COM 和 SendMessage 消息泵處理期間,線程A將被阻塞,直到線程B終止為止。
6) Interrupt()
中斷處於WaitSleepJoin線程狀態的線程。如果此線程當前未阻塞在等待、休眠或聯接狀態中,則下次開始阻塞時它將被中斷並引發ThreadInterruptedException異常。
線程應該捕獲ThreadInterruptedException並執行任何適當的操作以繼續運行。如果線程忽略該異常,則運行庫將捕獲該異常並停止該線程。
如果調用線程的 Interrupt()方法時線程正在執行非托管代碼,則運行庫將其標記為ThreadState.SuspendRequested。待線程返回到托管代碼時引發ThreadInterruptedException異常。
6. SpinWait(int iterations)
SpinWait實質上會將處理器置於十分緊密的自旋轉中,當前線程一直占用CPU,其循環計數由 iterations 參數指定。
SpinWait並不是一個阻止的方法:一個處於spin-waiting的線程的ThreadState不是WaitSleepJoin狀態,並且也不會被其它的線程過早的中斷(Interrupt)。SpinWait的作用是等待一個在極短時間(可能小於一微秒)內可准備好的可預期的資源,而避免調用Sleep()方法阻止線程而浪費CPU時間(上下文切換)。
優點:避免線程上下文切換的耗時操作。
缺點:CPU不能很好的調度CPU利用率。這種技術的優勢只能在多處理器計算機上體現,對單一處理器的電腦,直到輪詢的線程結束了它的時間片之前,別的資源無法獲得cpu調度執行。
7. 設置和獲取線程的單元狀態
// System.Threading.Thread 的單元狀態。 public enum ApartmentState { // System.Threading.Thread 將創建並進入一個單線程單元。 STA = 0, // System.Threading.Thread 將創建並進入一個多線程單元。 MTA = 1, // 尚未設置 System.Threading.Thread.ApartmentState 屬性。 Unknown = 2, }
1) 可使用ApartmentState獲取和設置線程的單元狀態,次屬性已經過時
2) SetApartmentState()+TrySetApartmentState()+GetApartentState()
可以標記一個托管線程以指示它將承載一個單線程或多線程單元。如果未設置該狀態,則GetApartmentState返回ApartmentState.Unknown。只有當線程處於ThreadState.Unstarted狀態時(即線程還未調用Start()時)才可以設置該屬性;一個線程只能設置一次。
如果在啟動線程之前未設置單元狀態,則該線程被初始化為默認多線程單元 (MTA)。(終結器線程和由ThreadPool控制的所有線程都是 MTA)
要將主應用程序線程的單元狀態設置為ApartmentState.STA的唯一方法是將STAThreadAttribute屬性應用到入口點方法。(eg:Main()方法)
線程使用托管線程本地存儲區 (TLS,Thread-Local Storage)來存儲線程特定的數據,托管 TLS 中的數據都是線程和應用程序域組合所獨有的,其他任何線程(即使是子線程)都無法獲取這些數據。
公共語言運行庫在創建每個進程時給它分配一個多槽數據存儲區數組,數據槽包括兩種類型:命名槽和未命名槽。
1) 若要創建命名數據槽,使用 Thread.AllocateNamedDataSlot() 或 Thread.GetNamedDataSlot() 方法。命名數據槽數據必須使用Thread.FreeNamedDataSlot()來釋放。
在任何線程調用Thread.FreeNamedDataSlot()之后,后面任何線程使用相同名稱調用Thread.GetNamedDataSlot()都將返回新槽。但是,任何仍具有以前通過調用Thread.GetNamedDataSlot()返回的System.LocalDataStoreSlot引用的線程可以繼續使用舊槽。
只有當調用Thread.FreeNamedDataSlot()之前獲取的所有LocalDataStoreSlot已被釋放並進行垃圾回收之后,與名稱關聯的槽才會被釋放。
2) 若要獲取對某個現有命名槽的引用,將其名稱傳遞給 Thread.GetNamedDataSlot() 方法。
3) 若要創建未命名數據槽,使用 Thread.AllocateDataSlot() 方法。未命名數據槽數據在線程終止后釋放。
4) 對於命名槽和未命名槽,使用 Thread.SetData() 和 Thread.GetData() 方法設置和檢索槽中的信息。
命名槽可能很方便,因為您可以在需要它時通過將其名稱傳遞給 GetNamedDataSlot 方法來檢索該槽,而不是維護對未命名槽的引用。但是,如果另一個組件使用相同的名稱來命名其線程相關的存儲區,並且有一個線程同時執行來自您的組件和該組件的代碼,則這兩個組件可能會破壞彼此的數據。(本方案假定這兩個組件在同一應用程序域內運行,並且它們並不用於共享相同數據。)
為了獲得更好的性能,請改用以 System.ThreadStaticAttribute特性標記的線程相關的靜態字段。
由於編譯器,或者CPU的優化,可能導致程序執行的時候並不是真正的按照代碼順序執行。在多線程開發的時候可能會引起錯誤。
在debug模式下,編譯器不會做任何優化,而當Release后,編譯器做了優化,此時就會出現問題。
1) Thread.MemoryBarrier()
按如下方式同步內存存取:執行當前線程的處理器在對指令重新排序時,不能采用先執行 Thread.MemoryBarrier()調用之后的內存存取,再執行 Thread.MemoryBarrier() 調用之前的內存存取的方式。
2) Thread.VolatileRead()+Thread.VolatileWrite() (內部使用MemoryBarrier()內存屏障)
a) VolatileRead() 讀取字段值。無論處理器的數目或處理器緩存的狀態如何,該值都是由計算機的任何處理器寫入的最新值。
b) VolatileWrite () 立即向字段寫入一個值,以使該值對計算機中的所有處理器都可見。
3) 關鍵字Volatile:
為了簡化編程,C#編譯器提供了volatile關鍵字。確保JIT編譯器對易失字段都以易失讀取或者易失寫入的方法執行,不用顯示調用Thread的VolatileRead()和VolatileWrite()。
10. BeginCriticalRegion()+EndCriticalRegion() (Critical:關鍵性的)
若要通知宿主代碼進入關鍵區域,調用BeginCriticalRegion。當執行返回到非關鍵代碼區域時,調用EndCriticalRegion。
公共語言運行庫 (CLR) 的宿主可在關鍵代碼區域和非關鍵代碼區域建立不同的失敗策略。關鍵區域是指線程中止或未處理異常的影響可能不限於當前任務的區域。相反,非關鍵代碼區域中的中止或失敗只對出現錯誤的任務有影響。
當關鍵區域中出現失敗時,宿主可能決定卸載整個AppDomain,而不是冒險在可能不穩定的狀態下繼續執行。
例如,假設有一個嘗試在占有鎖時分配內存的任務。如果內存分配失敗,則中止當前任務並不足以確保AppDomain的穩定性,原因是域中可能存在其他等待同一個鎖的任務。如果終止當前任務,則可能導致其他任務死鎖。
11. BeginThreadAffinity()+EndThreadAffinity() (Affinity:喜愛,密切關系)
使用BeginThreadAffinity和EndThreadAffinity方法通知宿主代碼塊依賴於物理操作系統線程的標識。
公共語言運行庫的某些宿主提供其自己的線程管理。提供其自己的線程管理的宿主可以在任何時候將正在執行的任務從一個物理操作系統線程移至另一個物理操作系統線程。大多數任務不會受此切換影響。但是,某些任務具有【線程關聯】 -- 即它們依賴於物理操作系統線程的標識。這些任務在其執行“不應被切換的代碼”時必須通知宿主。
例如,如果應用程序調用系統 API 以獲取具有【線程關聯】的操作系統鎖(如 Win32 CRITICAL_SECTION),則必須在獲取該鎖之前調用BeginThreadAffinity,並在釋放該鎖之后調用EndThreadAffinity。
還必須在從WaitHandle繼承的任何 .NET Framework 類型上發生阻止之前調用BeginThreadAffinity,因為這些類型依賴於操作系統對象。
線程本地存儲區和線程相關的靜態字段
可以使用托管線程本地存儲區 (TLS,Thread-Local Storage) 和線程相關的靜態字段來存儲某一線程和應用程序域所獨有的數據。
a) 如果可以在編譯時預料到確切需要,請使用線程相關的靜態字段。
b) 如果只能在運行時發現實際需要,請使用數據槽。
為了獲得更好的性能,請盡量改用以 System.ThreadStaticAttribute特性標記的線程相關的靜態字段。
無論是使用線程相關的靜態字段還是使用數據槽,托管 TLS 中的數據都是線程和應用程序域組合所獨有的。
a) 在應用程序域內部,一個線程不能修改另一個線程中的數據,即使這兩個線程使用同一個字段或槽時也不能。
b) 當線程從多個應用程序域中訪問同一個字段或槽時,會在每個應用程序域中維護一個單獨的值。
1) 線程相關的靜態字段(編譯時)
如果您知道某類型的字段【總是某個線程和應用程序域組合】所獨有的(即不是共享的),則使用ThreadStaticAttribute修飾靜態字段(static)。
需要注意的是,任何類構造函數代碼都將在訪問該字段的第一個上下文中的第一個線程上運行。在所有其他線程或上下文中,如果這些字段是引用類型,將被初始化為 null;如果這些字段是值類型,將被初始化為它們的默認值。因此,不要依賴於類構造函數來初始化線程相關的靜態字段[ThreadStatic]。相反,應總是假定與線程相關的靜態字段被初始化為 null 或它們的默認值。
2) 數據槽(運行時)
示例:托管TSL中數據的唯一性(數據槽|線程相關靜態字段)
/// <summary> /// 數據槽 的使用示例 /// </summary> private static void TLS4DataSlot() { LocalDataStoreSlot slot = Thread.AllocateNamedDataSlot("Name"); Console.WriteLine(String.Format("ID為{0}的線程,命名為\"Name\"的數據槽,開始設置數據。", Thread.CurrentThread.ManagedThreadId)); Thread.SetData(slot, "小麗"); Console.WriteLine(String.Format("ID為{0}的線程,命名為\"Name\"的數據槽,數據是\"{1}\"。" , Thread.CurrentThread.ManagedThreadId, Thread.GetData(slot))); Thread newThread = new Thread( () => { LocalDataStoreSlot storeSlot = Thread.GetNamedDataSlot("Name"); Console.WriteLine(String.Format("ID為{0}的線程,命名為\"Name\"的數據槽,在新線程為其設置數據 前 為\"{1}\"。" , Thread.CurrentThread.ManagedThreadId, Thread.GetData(storeSlot))); Console.WriteLine(String.Format("ID為{0}的線程,命名為\"Name\"的數據槽,開始設置數據。", Thread.CurrentThread.ManagedThreadId)); Thread.SetData(storeSlot, "小紅"); Console.WriteLine(String.Format("ID為{0}的線程,命名為\"Name\"的數據槽,在新線程為其設置數據 后 為\"{1}\"。" , Thread.CurrentThread.ManagedThreadId, Thread.GetData(storeSlot))); // 命名數據槽中分配的數據必須用 FreeNamedDataSlot() 釋放。未命名的數據槽數據隨線程的銷毀而釋放 Thread.FreeNamedDataSlot("Name"); } ); newThread.Start(); newThread.Join(); Console.WriteLine(String.Format("執行完新線程后,ID為{0}的線程,命名為\"Name\"的數據槽,在新線程為其設置數據 后 為\"{1}\"。" , Thread.CurrentThread.ManagedThreadId, Thread.GetData(slot))); }
// 不應依賴於類構造函數來初始化線程相關的靜態字段[ThreadStatic] [ThreadStatic] static string name = String.Empty; /// <summary> /// 線程相關靜態字段 的使用示例 /// </summary> private static void TLS4StaticField() { Console.WriteLine(String.Format("ID為{0}的線程,開始為name靜態字段設置數據。", Thread.CurrentThread.ManagedThreadId)); name = "小麗"; Console.WriteLine(String.Format("ID為{0}的線程,name靜態字段數據為\"{1}\"。", Thread.CurrentThread.ManagedThreadId, name)); Thread newThread = new Thread( () => { Console.WriteLine(String.Format("ID為{0}的線程,為name靜態字段設置數據 前 為\"{1}\"。", Thread.CurrentThread.ManagedThreadId, name)); Console.WriteLine(String.Format("ID為{0}的線程,開始為name靜態字段設置數據。", Thread.CurrentThread.ManagedThreadId)); name = "小紅"; Console.WriteLine(String.Format("ID為{0}的線程,為name靜態字段設置數據 后 為\"{1}\"。", Thread.CurrentThread.ManagedThreadId, name)); } ); newThread.Start(); newThread.Join(); Console.WriteLine(String.Format("執行完新線程后,ID為{0}的線程,name靜態字段數據為\"{1}\"。", Thread.CurrentThread.ManagedThreadId, name)); }
結果截圖:
.NET下未捕獲異常的處理
1. 控制台應用程序
通過為當前AppDomain添加 UnhandledException 事件處理程序。
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(UnhandledExceptionEventHandler); static void UnhandledExceptionEventHandler(object sender, UnhandledExceptionEventArgs e) { …… }
2. WinForm窗體應用程序
未處理的異常將引發Application.ThreadException事件。
a) 如果異常發生在主線程中,默認行為是未經處理的異常不終止該應用程序。在這種情況下,不會引發 UnhandledException 事件。但可以在在掛鈎 ThreadException 事件處理程序之前,使用應用程序配置文件或者使用 Application.SetUnhandledExceptionMode() 方法將模式設置為 UnhandledExceptionMode.ThrowException 來更改此默認行為。
b) 如果異常發生在其它線程中,將引發 UnhandledException 事件。
Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException) static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) { …… }
3. ASP.NET應用程序
要截獲ASP.NET 的未捕獲異常,我們需要為每個應用程序域安裝事件鈎子。這個過程需要分兩步完成:
a) 首先創建一個實現IHttpModule接口的類
public class UnhandledExceptionModule : IHttpModule { …… static object _initLock = new object(); static bool _initialized = false; public void Init(HttpApplication context) { // Do this one time for each AppDomain. lock (_initLock) { if (!_initialized) { AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(OnUnhandledException); _initialized = true; } } } }
b) 第二步:修改web.config,在 system.web 段中加入
<httpModules> <add name="UnhandledExceptionModule" type="WebMonitor.UnhandledExceptionModule" /> </httpModules>
判斷多個線程是否都結束的幾種方法
有園友問到此問題,所以提供下面幾種方法。若還有其他方法請告知。
1. 線程計數器
線程也可以采用計數器的方法,即為所有需要監視的線程設一個線程計數器,每開始一個線程,在線程的執行方法中為這個計數器加1,如果某個線程結束(在線程執行方法的最后),為這個計數器減1。使用這種方法需要使用原子操作(eg:Volatile、InterLocked)同步這個計數器變量。
2. 使用Thread.join方法
join方法只有在線程結束時才繼續執行下面的語句。可以對每一個線程調用它的join方法,但要注意,這個調用要在一個專門線程里做,而不要在主線程,否則程序會被阻塞。
3. 輪詢Thread的IsAlive屬性
IsAlive判斷此線程是否還存活。經測試只有 Unstarted、Stopped 返回false;其他線程狀態都返回true。
我們通過輪詢檢查此屬性來判斷線程是否結束。但要注意,這個調用要在一個專門線程里做,而不要在主線程,否則程序會被阻塞。
EG:while(true) { foreach(多個線程){ if(thread1.IsAlive) { } } }
4. 使用回調函數進行通知
5. 使用同步基元對象
Eg:WaitHandle。在后續章節中再說明
本博文主要為大家介紹了進程和線程的差別,計算機對多線程的支持,Thread類的詳解,線程狀態及影響線程狀態的各種線程操作,托管線程本地存儲區,線程中未處理異常的捕獲等等……
看完后你會發現如果程序任務小而多會造成不斷的創建和銷毀線程不便於線程管理;你可能還會發現當線程操作共享資源的時候沒有控制資源的同步問題……在后續章節中會陸續引入線程池和同步基元對象解決相應問題,敬請查看。
本節就此結束,謝謝大家查看,一起學習一起進步。
參考資料
擴展知識: