【轉】1.1非同步程式設計:執行緒概述及使用

sinolover發表於2020-12-01

 

clip_image001

         從此圖中我們會發現 .NET 與C# 的每個版本釋出都是有一個“主題”。即:C#1.0託管程式碼→C#2.0泛型→C#3.0LINQ→C#4.0動態語言→C#5.0非同步程式設計。現在我為最新版本的“非同步程式設計”主題寫系列分享,期待你的檢視及點評。

 

傳送門:非同步程式設計系列目錄……

 

開始:《非同步程式設計:執行緒概述及使用》

示例:非同步程式設計:執行緒概述及使用.rar

         做互動式客戶端應用程式,使用者總希望程式能時刻響應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.         常用屬性

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.         建立執行緒

1
2
3
4
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()啟動執行緒。必要的時候需在幫助器類中使用同步基元物件避免執行緒共享資料的死鎖和資源爭用。

2)         使用回撥方法檢索資料(見示例)

Thread建構函式接收的ThreadStart或ParameterizedThreadStart委託引數,這兩個委託的宣告都是返回void,即執行緒執行完後不會有資料返回(實際上主執行緒也不會等待Thread建立的新執行緒返回,否則建立新執行緒就無意義了)。那麼如何在非同步執行完時做出響應呢?使用回撥方法。

  

示例----關鍵程式碼(詳見Simple4CallBackWithParam()):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 包裝非同步方法的委託
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

1
2
3
4
5
6
7
8
9
public enum ThreadPriority
{
    Lowest = 0,
    BelowNormal = 1,
    // 預設情況下,執行緒具有 Normal 優先順序。
    Normal = 2,
    AboveNormal = 3,
    Highest = 4,
}

每個執行緒都具有分配給它的執行緒優先順序。在公共語言執行庫中建立的執行緒最初分配的優先順序為ThreadPriority.Normal。在執行庫外建立的執行緒會保留它們在進入托管環境之前所具有的優先順序。

執行緒是根據其優先順序而排程執行的。所有執行緒都是由作業系統分配處理器時間片的,如果具有相同優先順序的多個執行緒都可用,則計劃程式將遍歷處於該優先順序的執行緒,併為每個執行緒提供一個“固定的時間片”來執行,執行完“固定的時間片”後就切換執行緒,若當前任務還未執行完,則必須等待下一次的排程。

低優先順序的執行緒並不是被阻塞直到較高優先順序的執行緒完成,低優先順序的執行緒只是在相同時間間隔被CPU排程的次數相對較少。

重要提示:

         最好是降低一個執行緒的優先順序,而不是提升另一個執行緒的優先順序。如果執行緒要執行一個長時間執行的計算限制任務,比如編譯程式碼、拼寫檢查、電子表格重新計算等,一般應降低該執行緒的優先順序。如果執行緒要快速響應某個事件,然後執行非常短暫的時間,再恢復為等待狀態,則應提高該執行緒的優先順序。高優先順序執行緒在其生命中的大多數時間裡都應處於等待狀態,這樣才不至於影響系統的總體響應能力。

 

4.         執行緒狀態

Thread.ThreadState屬性提供一個位掩碼,用它指示執行緒的當前狀態。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[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

  

clip_image002

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())

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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。");

輸出:

clip_image004

         若在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.         設定和獲取執行緒的單元狀態

1
2
3
4
5
6
7
8
9
10
// 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.S他的唯一方法是將STAThreadAttribute屬性應用到入口點方法。(eg:Main()方法)

8.         設定和檢索執行緒資料(資料槽)

執行緒使用託管執行緒本地儲存區 (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特性標記的執行緒相關的靜態欄位。

9.         原子操作

由於編譯器,或者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)         資料槽(執行時)

         見上一小節(執行緒Thread類詳解)第8點分析

 

         示例:託管TSL中資料的唯一性(資料槽|執行緒相關靜態欄位)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// <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)));
}

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 不應依賴於類建構函式來初始化執行緒相關的靜態欄位[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));
}

 

        結果截圖:

    image

 

.NET下未捕獲異常的處理

1.         控制檯應用程式

         通過為當前AppDomain新增 UnhandledException 事件處理程式。

1
2
3
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 事件。

1
2
Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException)
static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)  { …… }

3.         ASP.NET應用程式

要截獲ASP.NET 的未捕獲異常,我們需要為每個應用程式域安裝事件鉤子。這個過程需要分兩步完成:

a)         首先建立一個實現IHttpModule介面的類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 段中加入

1
2
3
<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.         使用回撥函式進行通知

請參考“執行緒Thread類詳解”節第二點示例

5.         使用同步基元物件

     Eg:WaitHandle。在後續章節中再說明

 

 

 

本博文主要為大家介紹了程式和執行緒的差別,計算機對多執行緒的支援,Thread類的詳解,執行緒狀態及影響執行緒狀態的各種執行緒操作,託管執行緒本地儲存區,執行緒中未處理異常的捕獲等等……

看完後你會發現如果程式任務小而多會造成不斷的建立和銷燬執行緒不便於執行緒管理;你可能還會發現當執行緒操作共享資源的時候沒有控制資源的同步問題……在後續章節中會陸續引入執行緒池和同步基元物件解決相應問題,敬請檢視。

      本節就此結束,謝謝大家檢視,一起學習一起進步。

 

 

 

參考資料

                   MSDN

擴充套件知識:

                  Microsoft Windows 中的託管和非託管執行緒處理

                  多核程式設計偽共享問題及其對策

 

相關文章