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,或者最后把它放弃给其余线程时,线程才须要从托管代码转为内核代码。

相关文章
相关标签/搜索