CLR 線程同步

CLR 基元線程同步構造web

《CLR via C#》到了最後一部分,這一章重點在於線程同步,多個線程同時訪問共享數據時,線程同步能防止數據雖壞。之因此要強調同時,是由於線程同步問題其實就是計時問題。爲構建可伸縮的、響應靈敏的應用程序,關鍵在於不要阻塞你擁有的線程,使它們能用於(和重用於)執行其餘任務。編程

不須要線程同步是最理想的狀況,由於線程同步存在許多問題:緩存

  • 1 第一個問題是,它比較繁瑣,很容易出錯。安全

  • 2 第二個問題是,它們會損壞性能。獲取和釋放鎖是須要時間的,由於要調用一些額外的方法,並且不一樣的CPU 必須進行協調,以決定哪一個線程先取得鎖。讓機器中的CPU 以這種方式互相通訊,會對性能形成影響。服務器

    添加鎖後速度會慢下來,具體慢多少要取決於所選的鎖的種類。即使是最快的鎖,也會形成 方法 數倍地慢於沒有任何鎖的版本。多線程

  • 3 第三個問題在於,它們一次只容許一個線程訪問資源。這是鎖的所有意義之所在,但也是問題之所在,由於阻塞一個線程會形成更多的線程被建立。併發

 

線程同步如此的很差,應該如何在設計本身的應用時,儘可能避免線程同步呢?app

  • 具體就是避免使用像靜態字段這樣的共享數據。可試着使用值類型,由於它們老是被複制,每一個線程操做的都是它本身的副本。異步

  • 多個線程同時共享數據進行只讀訪問是沒有任何問題的。async

 

1 類庫和線程安全

Microsoft 的 Framework Class Library (FCL)保證全部靜態方法都是線程安全的。另外一方面,FCL 不保證明列方法是線程安全的。Jeffery Richter 建議你本身的類庫也遵循這個模式。這個模式有一點要注意:若是實例方法的目的是協調線程,則實例方法應該是線程安全的。

注意:使一個方法線程安全,並非說它必定要在內部獲取一個線程同步鎖。線程安全的方法意味着在兩個線程試圖同時訪問數據時,數據不會被破壞。例如:System.Math 類的一個靜態方法 Max。

 

2 基元用戶模式和 內核模式構造

基元(primitive)是指能夠在代碼中使用的最簡單的構造。有兩種基元構造:用戶模式(user-mode)和 內核模式(kernel-mode)。儘可能使用基元用戶模式構造,它們的速度要顯著快於內核模式構造。由於它們使用了特殊 CPU 指令來協調線程。這意味着協調是在硬件中發生的(因此才這麼快)。

但這意味着 Windows 系統永遠檢測不到一個線程在基元用戶模式的構造上阻塞了。因爲在用戶模式的基元構造上阻塞的線程池不認爲已阻塞,因此線程池不會建立新的線程來替換這種臨時阻塞的線程。此外,這些CPU 指令只阻塞線程至關短的時間。

 

3 用戶模式構造

CLR 保證對如下數據類型的變量讀寫是原子性的:Boolean,Char,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single以及引用類型。

舉個列子:

internal static class SomeTyoe{
public static Int32 x = 0;
}

若是一個線程執行這一行代碼:

SomeType.x = 0x01234567;

x 變量會一次性(原子性)地從0x00000000 變成0x01234567。另外一個線程不可能看處處於中間狀態的值。假定上述SomeType 類中的x 字段是一個Int64 ,那麼當一個線程執行如下代碼時:

SomeType.x = 0x0123456789abcdef

另外一個線程可能查詢x ,並獲得0x0123456700000000 或 0x0000000089abcdef 值,由於讀取和寫入操做不是原子性的。

 

雖然變量的原子訪問可保證讀取或寫入操做一次性完成,但因爲編譯器和CPU 的優化,不保證操做何時發生。本節討論的基元用戶模式構造,用於規劃好這些原子性讀取/寫入操做的時間。 此外,這些構造還可強制對(U)Int64 和 Double 類型的變量進行原子性的、規劃好了時間的訪問。

有兩種基於用戶模式線程同步構造。

  • 1 易變構造:在特定的時間,它在包含一個簡單數據類型的變量上 執行 原子性的讀 寫操做。

  • 2 互鎖構造:在特定的時間,它在包含一個簡單數據類型的變量上 執行 原子性的讀 寫操做。

全部易變 和 互鎖構造都要求傳遞對包含簡單數據類型的一個變量的引用(內存地址)。

 

3.1 易變構造 Volatile.Read 和 Volatile.Write

C# 對易變字段的支持

C# 編譯器提供了 volatile 關鍵字,它可應用於如下任何類型的靜態 或 實例字段:Boolean,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single和 Char。還可將 volatile 關鍵字應用於引用類型的字段,以及基礎類型爲 (S)Byte,(U)Int16,(U)Int32 的任何枚舉字段。

JIT 編譯器確保對易變字段的全部訪問都是易變讀取或寫入的方式執行,沒必要顯示調用 Volatile 的靜態 Read 或 Write 方法。另外,volatile 關鍵字告訴C# 和 JIT 編譯器不將字段緩存到CPU 的寄存器中,確保字段的全部讀寫操做都在 RAM 中進行。

下面是Volatile.Write 方法和 Volatile.Read 方法的使用。

internal sealed class ThreadsSharingData {
   private Int32 m_flag = 0;
   private Int32 m_value = 0;
   // This method is executed by one thread
   public void Thread1() {
       // Note: 5 must be written to m_value before 1 is written to m_flag
       m_value = 5;
       Volatile.Write(ref m_flag, 1);
  }
   // This method is executed by another thread
   public void Thread2() {
       // Note: m_value must be read after m_flag is read
       if (Volatile.Read(ref m_flag) == 1)
      Console.WriteLine(m_value);
  }
}
  • Volatile.Write 方法強迫location 中的值在調用時寫入。此外,按照編碼順序,以前的加載和存儲操做必須在調用 Volatile.Write 以前 發生。

  • Volatile.Read 方法強迫location 中的值在調用時讀取。此外,按照編碼順序,以後的加載和存儲操做必須在調用 Volatile.Read 以後 發生。

 

C# 對易變字段的支持

爲了簡化編程,C# 編譯器提供了 Volatile 關鍵字,它可應用於如下任何類型的靜態或實例字段:Boolean,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single 和 Char。還能夠將 Volatile 關鍵字應用於引用類型的字段,以及基礎類型爲(S)Byte,(U)Int16 或 (U)Int32 的任何枚舉字段。

volatile 關鍵字告訴 C# 和 JIT 編譯器不將字段緩存到 CPU 的寄存器中,確保字段的全部讀寫操做都在 RAM 中進行。

 

用 volatile 引發的很差事情:

  • 如:m_amount = m_amount + m_amount;

    //假定m_amount 是類中定義的一個volatile 字段。編譯器必須生成代碼將m_amount 讀入一個寄存器,再把它讀入另外一個寄存器,將兩個寄存器加到一塊兒,再將結果寫回 m_amount 字段。但最簡單的方式是將它的全部位都左移1 位。

  • 另外,C# 不支持以引用的方式將 volatile 字段傳給方法。

 

3.2 互鎖構造

本節將討論靜態System.Threading.Interlocked 類提供的方法。InterLocked 類中的每一個方法都執行一次原子讀取 以及 寫入操做。此外,Interlocked 的全部方法都創建了完整的內存柵欄(memory fence)。也就是說,調用某個 Interlocked 方法以前的任何變量寫入都在這個InterLocked 方法調用以前執行。而這個調用以後的任何變量讀取都在這個調用以後讀取。

做者很喜歡用 Interlocked 的方法,它們至關快,不阻塞任何線程。

AsyncCoordinator 可協調異步操做。做者給了個例子。

internal sealed class MultiWebRequests {
   // This helper class coordinates all the asynchronous operations
   private AsyncCoordinator m_ac = new AsyncCoordinator();
   // Set of web servers we want to query & their responses (Exception or Int32)
   // NOTE: Even though multiple could access this dictionary simultaneously,
   // there is no need to synchronize access to it because the keys are
   // read•only after construction
   private Dictionary<String, Object> m_servers = new Dictionary<String, Object> {
      { "http://Wintellect.com/", null },
      { "http://Microsoft.com/", null },
      { "http://1.1.1.1/", null }
  };
   
   public MultiWebRequests(Int32 timeout = Timeout.Infinite) {
       // Asynchronously initiate all the requests all at once
       var httpClient = new HttpClient();
       foreach (var server in m_servers.Keys) {
           m_ac.AboutToBegin(1);
           httpClient.GetByteArrayAsync(server).
           ContinueWith(task => ComputeResult(server, task));
  }
       // Tell AsyncCoordinator that all operations have been initiated and to call
       // AllDone when all operations complete, Cancel is called, or the timeout occurs
       m_ac.AllBegun(AllDone, timeout);
  }
   
   private void ComputeResult(String server, Task<Byte[]> task) {
   Object result;
   if (task.Exception != null) {
  result = task.Exception.InnerException;
  } else {
       // Process I/O completion here on thread pool thread(s)
       // Put your own compute•intensive algorithm here...
       result = task.Result.Length; // This example just returns the length
  }
   // Save result (exception/sum) and indicate that 1 operation completed
   m_servers[server] = result;
   m_ac.JustEnded();
}
// Calling this method indicates that the results don't matter anymore
public void Cancel() { m_ac.Cancel(); }
// This method is called after all web servers respond,
// Cancel is called, or the timeout occurs
private void AllDone(CoordinationStatus status) {
   switch (status) {
  case CoordinationStatus.Cancel:
  Console.WriteLine("Operation canceled.");
  break;
       case CoordinationStatus.Timeout:
           Console.WriteLine("Operation timed•out.");
           break;
       case CoordinationStatus.AllDone:
      Console.WriteLine("Operation completed; results below:");
  foreach (var server in m_servers) {
               Console.Write("{0} ", server.Key);
               Object result = server.Value;
               if (result is Exception) {
                   Console.WriteLine("failed due to {0}.", result.GetType().Name);
              } else {
                   Console.WriteLine("returned {0:N0} bytes.", result);
              }
  }
  break;
  }
  }
}
  • 1 調用 AsyncCoordinator 的 AboutToBegin 方法,向它傳遞要發出的請求數量。

  • 2 而後 調用 HttpClient 的GetByteArrayAsync 來初始化請求。在返回的 Task 上調用 ContinueWith ,確保在服務器上有了響應以後,個人 ComputeResult 方法可經過許多線程池線程併發處理結果。

  • 3 對Web 服務器的全部請求都發出以後,將調用 AsyncCoordinator 的 AllBegun 方法,向它傳遞要在全部操做完成後,執行的方法(AllDone)以及一個超時值。

  • 4 每收到一個Web 服務器響應,線程池線程都會調用 MultiWebRequests 的 ComputeResult 方法。該方法處理服務器返回的字節(或者發生的任何錯誤),將結果存到字典集合中。

  • 5 存好每一個結果以後,會調用 AsyncCoordinator 的 JustEnded 方法,使AsyncCoordintor 對象只讀一個操做已經完成。

  • 6 全部操做完成後,AsyncCoordinator 會調用AllDone 方法處理來自全部Web 服務器的結果。

  • 7 調用 AllDone 方法的是 哪一個線程?

    通常狀況 執行 AllDone 方法的線程就是獲取最後一個 Web服務器響應的哪一個線程池線程。

    但若是發生超時或取消,調用 AllDone 的線程就是 AsyncCoordinator 通知超時的 那個線程池線程,或是調用 Cancel 方法的那個線程。也有可能 AllDone 由發出 Web服務器請求的那個線程調用—— 若是最後一個請求在調用AllBegun 以前完成。

  • 8 在調用 AllBegun 方法時 存在竟態條件,由於如下事情可能剛好同時發生:

    • 1 所有操做結束

    • 2 發生超時

    • 3 調用Cancel

    • 4 調用 AllBegun

    這時 AsyncCoordinator 會選擇1 個贏家和 3 個輸家,確保AllDone 方法不被屢次調用。贏家是經過 傳給 AllDone 的 status 實參來識別的。

 

咱們來看一看 AsyncCoordinator 類的具體工做原理。AsyncCoordinator 類封裝了全部線程協調(合做)邏輯。它用 Interlocked 提供的方法來操做一切,確保代碼以極快的速度容許,同時沒有線程會被阻塞。

internal sealed class AsyncCoordinator {
   private Int32 m_opCount = 1; // Decremented when AllBegun calls JustEnded
   private Int32 m_statusReported = 0; // 0=false, 1=true
   private Action<CoordinationStatus> m_callback;
   private Timer m_timer;
   
   // This method MUST be called BEFORE initiating an operation
   public void AboutToBegin(Int32 opsToAdd = 1) {
  Interlocked.Add(ref m_opCount, opsToAdd);
  }
   
   // This method MUST be called AFTER an operation’s result has been processed
   public void JustEnded() {
       if (Interlocked.Decrement(ref m_opCount) == 0)
      ReportStatus(CoordinationStatus.AllDone);
  }
   
   // This method MUST be called AFTER initiating ALL operations
   public void AllBegun(Action<CoordinationStatus> callback,
  Int32 timeout = Timeout.Infinite) {
       m_callback = callback;
       if (timeout != Timeout.Infinite)
           m_timer = new Timer(TimeExpired, null, timeout, Timeout.Infinite);
       JustEnded();
  }
   
   private void TimeExpired(Object o) { ReportStatus(CoordinationStatus.Timeout); }
   public void Cancel() { ReportStatus(CoordinationStatus.Cancel); }
   
   private void ReportStatus(CoordinationStatus status) {
       // If status has never been reported, report it; else ignore it
       if (Interlocked.Exchange(ref m_statusReported, 1) == 0)
      m_callback(status);
  }
}

這個類最重要的字段就是 m_opCount 字段,用於跟蹤仍在進行的異步操做的數量。每一個異步操做開始前都會調用 AboutToBegin。該方法調用 Interlocked.Add,以原子方式將傳給它的數字加到 m_opCount 字段上。處理好Web 服務器的響應後會調用 JustEnded 。該方法調用Interlocked.Decrement,以原子方式從m_opCount 上減1。不管哪一個線程剛好將 m_opCount 設爲0,都由它調用ReportStatus。

ReportStatus 方法對所有操做結束、發生超時和調用Cancel 時可能發生的竟態條件進行仲裁。ReportStatus 必須確保其中只有一個條件勝出,確保 m_callback 方法只被調用一次。

 

3.3 實現簡單的自旋鎖

在多線程處理中,它意味着讓一個線程暫時「原地打轉」,以避免它跑去跟另外一個線程競爭資源。它會佔用CPU 資源

Interlocked 的方法很好用,但主要用於操做 Int32 值。若是須要原子性地操做類對象中的一組字段,又該怎麼辦? 這須要採起一個辦法阻止全部線程,只容許其中一個進入對字段進行操做的代碼區域,可使用 Interlocked 的方法構造一個線程同步塊:

internal struct SimpleSpinLock {
   private Int32 m_ResourceInUse; // 0=false (default), 1=true
   public void Enter() {
       while (true) {
           // Always set resource to in•use
           // When this thread changes it from not in•use, return
           if (Interlocked.Exchange(ref m_ResourceInUse, 1) == 0) return;
           // Black magic goes here...
      }
  }
   public void Leave() {
       // Set resource to not in-use
       Volatile.Write(ref m_ResourceInUse, 0);
  }
}

下面的類展現瞭如何使用 SimpleSpinLock.

public sealed class SomeResource {
   private SimpleSpinLock m_sl = new SimpleSpinLock();
   public void AccessResource() {
       m_sl.Enter();
       // Only one thread at a time can get in here to access the resource...
       m_sl.Leave();
  }
}

這種鎖的最大問題在於,在存在對鎖的競爭的前提下,會形成線程「自旋」。這個「自旋」會浪費寶貴的CPU 時間,阻止CPU 作其餘更有用的工做。所以自旋鎖只應該保護那些會執行得很是快的代碼區域。這種鎖通常不要在單 CPU 機器上使用。

爲了解決線程「自旋」 問題,許多自旋鎖內部有一些額外的邏輯。FCL 提供了一個名爲 System.Threading.SpinWait 的結構,封裝了人們關於這種 黑科技 的最新研究。

FCL 還包含一個 System.Threading.SpinLock 結構,它和 SimpleSpinLock 相似,只是使用了 SpinWait 結構來加強性能。 SpinLock 提供了超時支持。它們都是值類型。

 

3.4 Interlocked Anything 模式

使用 Interlocked.CompareExchagne 方法以原子方式在 Int32 上執行任何操做。 事實上,因爲 Interlocked.CompareExchange 提供了其餘重載版本,能操做 Int64 , Single, Double ,Object 和 泛型引用類型,因此該模式適合全部這些類型。

public static Int32 Maximum(ref Int32 target, Int32 value) {
   Int32 currentVal = target, startVal, desiredVal;
   // Don't access target in the loop except in an attempt
   // to change it because another thread may be touching it
   do {
       // Record this iteration's starting value
       startVal = currentVal;
       // Calculate the desired value in terms of startVal and value
       desiredVal = Math.Max(startVal, value);
       // NOTE: the thread could be preempted here!
       // if (target == startVal) target = desiredVal
       // Value prior to potential change is returned
       currentVal = Interlocked.CompareExchange(ref target, desiredVal, startVal);
       // If the starting value changed during this iteration, repeat
  } while (startVal != currentVal);
   
   // Return the maximum value when this thread tried to set it
   return desiredVal;
}

當這個操做進行時,其餘線程可能更改 target。雖然概率很小,但還是有可能發生的。若是真的發生,desiredVal 的值就是基於存儲在 startVal 中的舊值而得到的,而非基於 target 的新值。這時就不該該更改 target 。咱們用 interlocked.CompareExchange 方法確保沒有其餘線程更改 target 的前提下 將target 的值改成 desiredVal。

 

4 內核模式

內核模式的構造更慢,有兩個緣由:

  • 1 它們要求 Windows 操做系統自身的配合

  • 2 在內核對象上調用的每一個方法都形成調用線程從託管代碼轉換爲 本機用戶模式代碼。再轉換爲本機內核模式代碼。

但內核模式的構造具有基元用戶模式構造不具有的優勢。

  • 1 內核模式的構造檢測到一個資源上的競爭時,Windows 會阻塞輸掉的線程,使它不佔着一個 CPU 「自旋」,無畏地浪費處理器資源。

  • 2 內核模式的構造可實現本機(native)和託管(managed)線程相互之間的同步。

  • 3 內核模式的構造可同步在同一臺機器的不一樣進程中運行的線程。

  • 4 內核模式的構造可應用安全性設置,爲防止未經受權的帳戶訪問它們。

  • 5 在內核模式的構造上阻塞的線程可指定超時值。指定時間內訪問不到但願的資源,線程就能夠解除阻塞並執行其餘任務。

 

內核模式基元構造一共兩種:事件 和 信號量。至於其餘內核模式構造,好比 互斥體,則是在兩個基元構造上構建的。

System.Threading 命名空間提供了一個名爲 WaitHandle 抽象基類,它包裝了一個 Windows 內核對象句柄。在一個內核模式 的構造上調用的每一個方法都表明一個完整的內存柵欄。WaitHandle 基類內部有一個 SafeWaitHandle 字段,它容納了一個 Win32 內核對象句柄。這個字段是在構造一個具體的WaitHandle 派生類時初始化的。

AutoResetEvent , ManualResetEvent,Semaphore 和 Mutex 類 都派生自 WaitHandle ,它們繼承了 WaitHandle 的方法和行爲。

using System;
using System.Threading;

public static class Program {
   public static void Main() {
       Boolean createdNew;
       // Try to create a kernel object with the specified name
       using (new Semaphore(0, 1, "SomeUniqueStringIdentifyingMyApp", out createdNew)) {
           if (createdNew) {
               // This thread created the kernel object so no other instance of this
               // application must be running. Run the rest of the application here...
          } else {
               // This thread opened an existing kernel object with the same string name;
               // another instance of this application must be running now.
               // There is nothing to do in here, let's just return from Main to terminate
               // this second instance of the application.
          }
      }
  }
}

上述代碼使用的是 Semaphore,但換成EventWaitHandle 或 Mutex 同樣也能夠,由於我並無真正使用對象提供的線程同步行爲。但我利用了在建立任何種類的內核對象時由Windows 內核提供的一些線程同步行爲。當兩個進程中的線程都嘗試建立具備相同字符串名稱的一個Semaphore,Windows 內核確保只有一個線程實際地建立具備指定名稱的內核對象。建立對象的線程會將它的 createdNew 變量設爲true。

 

4.1 Event 構造

事件(event)其實只是由內核維護的 Boolen 變量。事件爲 false, 在事件上等待的線程就阻塞;事件爲 true ,就解除阻塞。有兩種事件,即自動重置事件和 手動重置事件。自動重置事件爲 true 時,它只喚醒一個阻塞的線程。手動重置事件爲 true時,它解除正在等待它的全部線程的阻塞,由於內核不將事件自動重置回false。必須手動重置回false。

public class EventWaitHandle : WaitHandle {
  public Boolean Set(); // Sets Boolean to true; always returns true
  public Boolean Reset(); // Sets Boolean to false; always returns true
}

public sealed class AutoResetEvent : EventWaitHandle {
public AutoResetEvent(Boolean initialState);
}

public sealed class ManualResetEvent : EventWaitHandle {
public ManualResetEvent(Boolean initialState);
}

可用自動重置事件輕鬆建立線程同步鎖,它的行爲和前面展現的 SimpleSpinLock 相似:

internal sealed class SimpleWaitLock : IDisposable {
   private readonly AutoResetEvent m_available;
   
   public SimpleWaitLock() {
  m_available = new AutoResetEvent(true); // Initially free
  }
   
   public void Enter() {
       // Block in kernel until resource available
       m_available.WaitOne();
  }
   
   public void Leave() {
       // Let another thread access the resource
       m_available.Set();
  }
   
   public void Dispose() { m_available.Dispose(); }
}

和使用 SimlpeSpinLock 時徹底同樣的方式使用 SimpleWaitLock,表面上徹底相同,可是兩個鎖的性質大相徑庭。鎖上面沒有競爭的時候, SimpleWaitLock 比 SimpleSpinLock 慢得多,由於對 SimpleWaitLock 的 Enter 和 Leave 方法的每一個調用都強迫線程從託管代碼轉換爲內核代碼。再轉換回來。但在存在競爭的時候,輸掉的線程會被內核阻塞,不會在那裏自旋,從而不浪費CPU 事件。

 

Semaphore 構造

信號量(semaphore)其實就是內核維護的Int32 變量。信號量爲 0 時,在信號量上等待的線程會阻塞;信號量大於 0 時解除阻塞。在信號量上等待的線程解除阻塞時,內核自動從信號量 的計數中減 1。信號量還關聯了一個最大 Int32 值,當前計數毫不容許超過最大計數。下面展現了 Semaphore 類的樣子:

public sealed class Semaphore : WaitHandle {
   public Semaphore(Int32 initialCount, Int32 maximumCount);
   public Int32 Release(); // Calls Release(1); returns previous count
   public Int32 Release(Int32 releaseCount); // Returns previous count
}

 

總結一下這三種內核構造基元的行爲:

  • 多個線程在一個自動重置事件上等待時,設置事件只致使一個線程被解除阻塞。

  • 多個線程在一個手動重置事件上等待時,設置事件致使全部線程被解除阻塞。

  • 多個線程在一個信號量上等待時,釋放信號量致使 releaseCount 個線程被解除阻塞(

    releaseCount 是傳給 Semaphore 的 Release 方法的實參)。

自動重置事件和信號量的區別是:

能夠在一個自動重置事件上連續屢次調用 Set,同時仍然只有一個線程解除阻塞。相反,在一個信號量上連續屢次調用Release ,會使它的內部計數一直遞增,這可能解除大量線程的阻塞。順便說一句,若是在一個信號量上屢次調用Release ,會致使它的計數超過最大計數,這時Release 會拋出一個 SemaphoreFullException。

可像下面這樣用信號量從新實現 SimpleWaitLock,容許多個線程併發訪問一個資源。

public sealed class SimpleWaitLock : IDisposable {
   private readonly Semaphore m_available;
   
   public SimpleWaitLock(Int32 maxConcurrent) {
  m_available = new Semaphore(maxConcurrent, maxConcurrent);
  }
   
   public void Enter() {
       // Block in kernel until resource available
       m_available.WaitOne();
  }
   
   public void Leave() {
       // Let another thread access the resource
       m_available.Release(1);
  }
   public void Dispose() { m_available.Close(); }
}

 

Mutex Constructs

互斥體(mutex)表明一個互斥的鎖。它的工做方式和 AutoResetEvent (或者技術爲1 的 Semaphore )類似,三者都是一次只釋放一個正在等待的線程。下面是 Mutex 類的樣子:

public sealed class Mutex : WaitHandle {
   public Mutex();
   public void ReleaseMutex();
}

互斥體有一些額外的邏輯,這形成它們比其餘構造更復雜。一個是記錄被哪一個線程ID記錄了,一個是記錄被線程調用的次數。

1 Mutex 對象會查詢調用線程的 Int32 ID ,記錄是哪一個線程得到了它。一個線程調用 ReleaseMutex 時,Mutex 確保調用線程就是獲取 Mutex 的那個線程。如諾否則,Mutex 對象的狀態就不會改變,而ReleaseMutex 會拋出一個 System.ApplicationException。另外,擁有Mutex 的線程由於任何緣由而終止,在Mutex 上等待的某個線程會由於拋出 System.Threading.AbandonedMutexException 異常而被喚醒。該異常一般會成爲未處理的異常,從而終止整個進程。

2 Mutex 對象維護着一個遞歸計數,指出擁有該 Mutex 的線程擁有了它多少次。若是一個線程當前擁有一個 Mutex,然後線程再次在 Mutex 上等待,計數就會遞增,這個線程容許繼續運行。線程調用 ReleaseMutex 將致使計數遞減。只有計數變成 0,另外一個線程才能爲該 Mutex 的全部者。

Mutex 對象須要更多的內存來容納額外的線程 ID 和計數信息。Mutex 必須維護這些信息,使鎖變得更慢。

一般當一個方法獲取了一個鎖,而後調用也須要這個鎖的另外一個方法,就須要一個遞歸鎖。下面的代碼要釋放兩次,其餘線程才能得到該鎖。代碼以下所示。

internal class SomeClass : IDisposable {
   private readonly Mutex m_lock = new Mutex();
   
   public void Method1() {
       m_lock.WaitOne();
       // Do whatever...
       Method2(); // Method2 recursively acquires the lock
       m_lock.ReleaseMutex();
  }
   
   public void Method2() {
       m_lock.WaitOne();
       // Do whatever...
       m_lock.ReleaseMutex();
  }

public void Dispose() { m_lock.Dispose(); }
}

 

若是SomeClass 使用一個 AutoResetEvent 而不是 Mutex,線程在調用Method2 的WaitOne 方法時會阻塞。

若是須要遞歸鎖,可使用一個 AutoResetEvent 來簡單建立一個:

internal sealed class RecursiveAutoResetEvent : IDisposable {
   private AutoResetEvent m_lock = new AutoResetEvent(true);
   private Int32 m_owningThreadId = 0;
   private Int32 m_recursionCount = 0;
   
   public void Enter() {
       // Obtain the calling thread's unique Int32 ID
       Int32 currentThreadId = Thread.CurrentThread.ManagedThreadId;
       // If the calling thread owns the lock, increment the recursion count
       if (m_owningThreadId == currentThreadId) {
           m_recursionCount++;
           return;
  }
       // The calling thread doesn't own the lock, wait for it
       m_lock.WaitOne();
       // The calling now owns the lock, initialize the owning thread ID & recursion count
       m_owningThreadId = currentThreadId;
       m_recursionCount = 1;
  }
   
   public void Leave() {
       // If the calling thread doesn't own the lock, we have an error
       if (m_owningThreadId != Thread.CurrentThread.ManagedThreadId)
      throw new InvalidOperationException();
       // Subtract 1 from the recursion count
       if (--m_recursionCount == 0) {
           // If the recursion count is 0, then no thread owns the lock
           m_owningThreadId = 0;
           m_lock.Set(); // Wake up 1 waiting thread (if any)
      }
  }
   
   public void Dispose() { m_lock.Dispose(); }
}

雖然 RecursiveAutoResetEvent 類的行爲和 Mutex 類徹底同樣,但在一個線程試圖遞歸取鎖時,它大的性能會好不少,由於如今跟蹤線程全部權和遞歸的都是託管代碼。只有在一次獲取AutoResetEvent,或者最後把它放棄給其餘線程時,線程才須要從託管代碼轉爲內核代碼。

相關文章
相關標籤/搜索