CLR 线程同步

Posted mingjie-c

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CLR 线程同步相关的知识,希望对你有一定的参考价值。

CLR 基元线程同步构造

《CLR via C#》到了最后一部分,这一章重点在于线程同步,多个线程同时访问共享数据时,线程同步能防止数据虽坏。之所以要强调同时,是因为线程同步问题其实就是计时问题。为构建可伸缩的、响应灵敏的应用程序,关键在于不要阻塞你拥有的线程,使它们能用于(和重用于)执行其他任务。

不需要线程同步是最理想的情况,因为线程同步存在许多问题:

  • 1 第一个问题是,它比较繁琐,很容易出错。

  • 2 第二个问题是,它们会损坏性能。获取和释放锁是需要时间的,因为要调用一些额外的方法,而且不同的CPU 必须进行协调,以决定哪个线程先取得锁。让机器中的CPU 以这种方式互相通信,会对性能造成影响。

    添加锁后速度会慢下来,具体慢多少要取决于所选的锁的种类。即便是最快的锁,也会造成 方法 数倍地慢于没有任何锁的版本。

  • 3 第三个问题在于,它们一次只允许一个线程访问资源。这是锁的全部意义之所在,但也是问题之所在,因为阻塞一个线程会造成更多的线程被创建。

 

线程同步如此的不好,应该如何在设计自己的应用时,尽量避免线程同步呢?

  • 具体就是避免使用像静态字段这样的共享数据。可试着使用值类型,因为它们总是被复制,每个线程操作的都是它自己的副本。

  • 多个线程同时共享数据进行只读访问是没有任何问题的。

 

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

以上是关于CLR 线程同步的主要内容,如果未能解决你的问题,请参考以下文章

《CLR via C#》之线程处理——任务调度器

多线程 Thread 线程同步 synchronized

CLR via C# 阅读 笔记

线程同步-使用ReaderWriterLockSlim类

CLR基础与术语

随笔CLR:向SBI迈进一大步!!!