CLR via C#, 4th -- 【线程处理】 -- 第30章混合线程同步构造

Power Threading Library

30.1 —个简单的混合锁

internal sealed class SimpleHybridLock : IDisposable { 
   // The Int32 is used by the primitive user­mode constructs (Interlocked methods) 
   private Int32 m_waiters = 0;  
 
   // The AutoResetEvent is the primitive kernel ­mode construct 
   private readonly AutoResetEvent m_waiterLock = new AutoResetEvent(false);  
 
   public void Enter() { 
      // Indicate that this thread wants the lock 
      if (Interlocked.Increment(ref m_waiters) == 1) 
         return; // Lock was free, no contention, just return  
 
      // Another thread has the lock (contention), make this thread wait 
      m_waiterLock.WaitOne();  // Bad performance hit here  
      // When WaitOne returns, this thread now has the lock 
   } 
 
   public void Leave() { 
      // This thread is releasing the lock 
      if (Interlocked.Decrement(ref m_waiters) == 0) 
         return; // No other threads are waiting, just return  
 
      // Other threads are waiting, wake 1 of them 
      m_waiterLock.Set();  // Bad performance hit here 
   } 
 
   public void Dispose() { m_waiterLock.Dispose(); } 
}

调用Enter的第一个线程造成Interlocked.Increment在m_waiters字段上加1,使它的值变成1,这个线程发现以前有零个线程正在等待这个锁,所以线程从它的Enter调用中返回。值得欣赏的是,线程获得锁的速度非常快。现在,如果另一个线程介入并调用Enter,这个线程将m_waiters递增到2,发现锁在另一个线程那里。所以,这个线程会使用AutoResetEvent对象来调用WaitOne,从而阻塞自身。调用WaitOne造成线程的代码转变成内核模式的代码,这会对性能产生巨大影响。但线程反正都要停止运行,所以让线程花点时间来完全停止,似乎也不是太坏。好消息是,线程现在会阻塞,不会因为在CPU上“自旋”而浪费CPU时间。(29.3.3节“实现简单的自旋锁”引入的SimpleSpinLock的Enter方法就会这样“自旋”。)
再来看看Leave方法。一个线程调用Leave时,会调用Interlocked.Decrement从m_waiters字段减1。如果mwaiters现在是0,表明没有其他线程在调用Enter时发生阻塞,调用Leave的线程可以直接返回。同样地,想象一下这有多快:离开一个锁意味着线程从一个Int32中减1,执行快速的if测试,然后返回!另一方面,如果调用Leave的线程发现m_waiters不为0,线程就知道现在存在一个竞争,另外至少有一个线程在内核中阻塞。这个线程必须唤醒一个(而且只能是一个)阻塞的线程。唤醒线程是通过在AutoResetEvent上调用Set来实现的。这会造成性能上的损失,因为线程必须转换成内核模式代码,再转换回来。但这个转换只有在发生竞争时才会发生。当然,AutoResetEvent确保只有一个阻塞的线程被唤醒:在AutoResetEvent上阻塞的其他所有线程会继续阻塞,直到新的、解除了阻塞的线程最终调用Leave。

30.2 自旋、线程所有权和递归

由于转换为内核模式会造成巨大的性能损失,而且线程占有锁的时间通常都很短,所以为了提升应用程序的总体性能,可以让一个线程在用户模式中“自旋”一小段时间,再让线程转换为内核模式。如果线程正在等待的锁在线程“自旋”期间变得可用,就能避免向内核模式的转换了。
此外,有的锁限制只能由获得锁的线程释放锁。有的锁允许当前拥有它的线程递归地拥有锁(多次拥有),Mutex锁就是这样的一个例子。

internal sealed class AnotherHybridLock : IDisposable { 
   // The Int32 is used by the primitive user­mode constructs (Interlocked methods) 
   private Int32 m_waiters = 0;  
 
   // The AutoResetEvent is the primitive kernel ­mode construct 
   private AutoResetEvent m_waiterLock = new AutoResetEvent(false); 
 
   // This field controls spinning in an effort to improve performance 
   private Int32 m_spincount = 4000;   // Arbitrarily chosen count  
 
   // These fields indicate which thread owns the lock and how many times it owns it 
   private Int32 m_owningThreadId = 0, m_recursion = 0; 
 
   public void Enter() { 
      // If calling thread already owns the lock, increment recursion count and return  
      Int32 threadId = Thread.CurrentThread.ManagedThreadId; 
      if (threadId == m_owningThreadId) { m_recursion++; return; }  
 
      // The calling thread doesn't own the lock, try to get it  
      SpinWait spinwait = new SpinWait();  
      for (Int32 spinCount = 0; spinCount < m_spincount; spinCount++) {  
         // If the lock was free, this thread got it; set some state and return 
         if (Interlocked.CompareExchange(ref m_waiters, 1, 0) == 0) goto GotLock;  
 
         // Black magic: give other threads a chance to run  
         // in hopes that the lock will be released  
         spinwait.SpinOnce(); 
      } 
 
      // Spinning is over and the lock was still not obtained, try one more time 
      if (Interlocked.Increment(ref m_waiters) > 1) {  
         // Still contention, this thread must wait  
         m_waiterLock.WaitOne(); // Wait for the lock; performance hit 
         // When this thread wakes, it owns the lock; set some state and return 
      } 
 
   GotLock:  
      // When a thread gets the lock, we record its ID and  
      // indicate that the thread owns the lock once 
      m_owningThreadId = threadId; m_recursion = 1;  
   } 
  
   public void Leave() { 
      // If the calling thread doesn't own the lock, there is a bug 
      Int32 threadId = Thread.CurrentThread.ManagedThreadId; 
      if (threadId != m_owningThreadId) 
         throw new SynchronizationLockException("Lock not owned by calling thread"); 
           // Decrement the recursion count. If this thread still owns the lock, just return 
           
      if (­­m_recursion > 0) return; 
 
      m_owningThreadId = 0;   // No thread owns the lock now 
 
      // If no other threads are waiting, just return  
      if (Interlocked.Decrement(ref m_waiters) == 0)   
         return;  
 
      // Other threads are waiting, wake 1 of them 
      m_waiterLock.Set();     // Bad performance hit here 
   } 
 
   public void Dispose() { m_waiterLock.Dispose(); } 
}
 

30.3 FCL中的混合构造

30.3.1 ManualResetEventSlim类和SemaphoreSlim类

System.Threading.ManualResetEventSlim和System.Threading.SemaphoreSlim这两个类。这两个构造的工作方式和对应的内核模式构造完全一致,只是它们都在用户模式中“自旋”,而且都推迟到发生第一次竞争时,才创建内核模式的构造。它们的Wait方法允许传递一个超时值和一个CancellationToken。

public class ManualResetEventSlim : IDisposable { 
   public ManualResetEventSlim(Boolean initialState, Int32 spinCount); 
   public void Dispose(); 
   public void Reset();  
   public void Set();  
   public Boolean Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken); 
 
   public Boolean IsSet { get; } 
   public Int32 SpinCount { get; } 
   public WaitHandle WaitHandle { get; } 
} 
public class SemaphoreSlim : IDisposable { 
   public SemaphoreSlim(Int32 initialCount, Int32 maxCount); 
   public void Dispose(); 
   public Int32 Release(Int32 releaseCount); 
   public Boolean Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken); 
 
   // Special method for use with async and await (see Chapter 28)  
   public Task<Boolean> WaitAsync(Int32 millisecondsTimeout, CancellationToken 
cancellationToken); 
 
   public Int32 CurrentCount { get; } 
   public WaitHandle AvailableWaitHandle { get; } 
}

30.3.2 Monitor 和同步块

最常用的混合型线同步构造就是Monitor类,它提供了支持自旋、线程所有权和递归的互斥锁。
同步块
堆中的每个对象都可关联一个名为同步块的数据结构。
同涉块包含字段,和AnotherHybridLock类的字段相似。具体地说,它为内核对象、拥有线程(owning thread)的ID、递归计数(recursion count)以及等待线程(waiting thread)计数提供了相应的字段。
Monitor是静态类,它的方法接受对任何堆对象的引用。这些方法对指定对象的同步块中的字段进行操作。

public static class Monitor { 
   public static void Enter(Object obj); 
   public static void Exit(Object obj); 
 
   // You can also specify a timeout when entered the lock (not commonly used): 
   public static Boolean TryEnter(Object obj, Int32 millisecondsTimeout);  
 
   // I’ll discuss the lockTaken argument later 
   public static void Enter(Object obj, ref Boolean lockTaken);  
   public static void TryEnter(Object obj, Int32 millisecondsTimeout, ref Boolean lockTaken);  
}

每当一个对象在堆中创建的时候,都有两个额外的开销字段与它关联。第一个是“类型对象指针”,包含类型的“类型对象”的内存地址。第二个是“同步块索引”,包含同步块数组中的一整数索引。

一个对象在构造时,它的同步块索引初始化为-1,表明不引用任何同步块。然后,调用Monitor.Enter时,CLR在数组中找到一个空白同步块,并设置对象的同步块索引,让它引用该同步块。换言之,同步块和对象是动态关联的。调用Exit时,会检查是否有其他任何线程正在等待使用对象的同步块。如果没有线程在等待它,同步块就自由了,Exit将对象的同步块索引设回-1,自由的同步块将来可以和另一个对象关联。

 上图展示了堆中的对象、它们的同步块索引以及CLR的同步块数组元素之间的关系。Object-A,Object-B和Object-C都将它们的类型对象指针成员设为引用Type-T(一个类型对象)。这意味着三个对象全都具有相同的类型。如第4章所述,类型对象本身也是堆中的一个对象。和其他所有对象一样,类型对象有两个开销成员:同步块索引和类型对象指针。这意味着同步块可以和类型对象关联,而且可以将一个类型对象引用传给Monitor的方法。顺便说一句,如有必要,同步块数组能创建更多的同步块。所以,同时同步大量对象时,不必担心系统会用光同步块。

internal sealed class Transaction { 
   private DateTime m_timeOfLastTrans;  
 
   public void PerformTransaction() { 
      Monitor.Enter(this);  
      // This code has exclusive access to the data... 
      m_timeOfLastTrans = DateTime.Now; 
      Monitor.Exit(this); 
   } 
 
   public DateTime LastTransaction {  
      get {  
         Monitor.Enter(this); 
         // This code has exclusive access to the data... 
         DateTime temp = m_timeOfLastTrans;  
         Monitor.Exit(this);  
         return temp;  
      } 
   } 
}

上述代码表面看很简单,但实际存在问题。现在的问题是,每个对象的同步块索引都隐式为公共的。以下代码演示了这可能造成的影响。

public static void SomeMethod() {  
   var t = new Transaction(); 
   Monitor.Enter(t); // This thread takes the object's public lock  
 
   // Have a thread pool thread display the LastTransaction time 
   // NOTE: The thread pool thread blocks until SomeMethod calls Monitor.Exit!  
   ThreadPool.QueueUserWorkItem(o => Console.WriteLine(t.LastTransaction)); 
 
   // Execute some other code here...    
   Monitor.Exit(t); 
}

在上述代码中,执行SomeMethod的线程调用Monitor.Enter,获取由Transaction对象公开的锁。线程池线程查询LastTransaction属性时,这个属性也调用Monitor.Enter来获取同一个锁,造成线程池线程阻塞,直到执行SomeMethod的线程调用Monitor.Exit,有调试器可发现线程池线程在LastTransaction属性内部阻塞。但很难判断是另外哪个线程拥有锁。即使真的弄清楚了是哪个线程拥有锁,还必须弄清楚是什么代码造成它取得锁,这就更难了。更糟的是,即使历经千辛万苦,终于搞清楚了是什么代码造成线程取得锁,最后却发现那些代码不在你的控制范围之内,或者无法修改它们来修正问题。因此,我的建议是始终坚持使用私有锁。下面展示了如何修正Transaction类:

internal sealed class Transaction { 
   private readonly Object m_lock = new Object(); // Each transaction has a PRIVATE lock now 
   private DateTime m_timeOfLastTrans;  
 
   public void PerformTransaction() { 
      Monitor.Enter(m_lock);     // Enter the private lock  
      // This code has exclusive access to the data... 
      m_timeOfLastTrans = DateTime.Now; 
      Monitor.Exit(m_lock);      // Exit the private lock 
   } 
 
   public DateTime LastTransaction {  
      get {  
         Monitor.Enter(m_lock);  // Enter the private lock  
         // This code has exclusive access to the data... 
         DateTime temp = m_timeOfLastTrans;  
         Monitor.Exit(m_lock);   // Exit the private lock 
         return temp;  
      } 
   } 
}

如果Transaction的成员是静态的,只需将mlock字段也变成静态字段,即可确保静态成员的线程安全性。

Monitor根本就不该实现成静态类;它应该像其他所有同步构造那样实现。也就是说,应该是一个可以实例化并在上面调用实例方法的类。事实上,正因为Monitor被设计成一个静态类,所以它还存在以下等等许多问题。

  • 变量能引用一个代理对象-前提是变量引用的那个对象的类型派生自System.MarshalByRefObject类(参见第22章"CLR寄宿和AppDomain")。调用Monitor的方法时,传递对代理对象的引用,锁定的是代理对象而不是代理引用的实际对象。
  • 如果线程调用Monitor.Enter,向它传递对类型对象的引用,而且这个类型对象是以"AppDomain中立”的方式加载的,线程就会跨越进程中的所有AppDomain在那个类型上获取锁。
  • 由于字符串可以留用(参见14.2.2节“字符串是不可变的”),所以两个完全独立的代码段可能在不知情的情况下获取对内存中的一个String对象的引用。如果将这个String对象引用传给Monitor的方法,两个独立的代码段现在就会在不知情的情况下以同步方式执行。
  • 跨越AppDomain边界传递字符串时,CLR不创建字符串的副本;相反,它只是将对字符串的一个引用传给其他AppDomain,这增强了性能,理论上也是可行的,因为String对象本来就不可变(不可修改)。但和其他所有对象一样,String对象关联了一个同步块索引,这个索引是可变的(可修改),使不同AppDomain中的线程在不知情的情况下开始同步。
  • 由于Monitor的方法要获取一个Object,所以传递值类型会导致值类型被装箱,造成线程在已装箱对象上获取锁。每次调用Monitor.Enter都会在一个完全不同的对象上获取锁,造成完全无法实现线程同步。
  • 向方法应用[MethodImpl(MethodlmplOptions.Synchronized)]特性,会造成JIT编译器用Monitor.Enter和Monitor.Exit调用包围方法的本机代码。如果方法是实例方法,会将this传给Monitor的这些方法,锁定隐式公共的锁。如果方法是静态的,对类型的类型对象的引用会传给这些方法,造成锁定"AppDomain中立”的类型。我的建议是永远不要使用这个特性。
  • 调用类型的类型构造器时(参见8.3节“类型构造器”),CLR要获取类型对象上的一个锁,确保只有一个线程初始化类型对象及其静态字段。同样地,这个类型可能以"AppDomain中立”的方式加载,所以会出问题。例如,假定类型构造器的代码进入死循环,进程中的所1 AppDomain都无法使用该类型。我的建议是尽量避免使用类型构造器,或者全少保持它们的短小和简单。

lock语句

private void SomeMethod() { 
   lock (this) {  
      // This code has exclusive access to the data... 
   } 
}
It is equivalent to having written the method like this.
private void SomeMethod() { 
   Boolean lockTaken = false; 
   try {  
      // An exception (such as ThreadAbortException) could occur here... 
      Monitor.Enter(this, ref lockTaken);  
      // This code has exclusive access to the data... 
   } 
   finally { 
      if (lockTaken) Monitor.Exit(this); 
   } 
}

第一个问题是,C#团队认为他们在finally块中调用Monitor.Exit是帮了你一个大忙。他们的想法是,这样一来,就可确保锁总是得以释放,无论try块中发生了什么。但这只是他们一厢情愿的想法。在try块中,如果在更改状态时发生异常,这个状态就会处于损坏状态。锁在finally块中退出时,另一个线程可能开始操作损坏的状态。显然,更好的解决方案是让应用程序挂起,而不是让它带着损坏的状态继续运行。这样不仅结果难以预料,还有可能引发安全隐患。

第二个问题是,进入和离开try块会影响方法的性能。有的JT编译器不会内联含有try块的方法,造成性能进一步下降。所以最终结果是,不仅代码的速度变慢了,还会造成线程访问损坏的状态。"我的建议是杜绝使用C#的lock语句。
lockTaken变量
假定一个线程进入try块,但在调用Monitor.Enter之前退出(参见第22章)。现在,finally块会得到调用,但它的代码不应退出锁。lockTaken变量就是为了解决这个问题而设计的。它初始化为false,假定现在还没有进入锁(还没有获得锁),然后,如果调用Monitor.Enter,而且成功获得锁,Enter方法就会将lockTaken设为true.finally块通过检查lockTaken,便知道到底要不要调用Monitor.Exit。顺便说一句,SpinLock结构也支持这个lockTaken模式。

30.3.3 ReaderWriterLockSlim类

ReaderWriterLockSlim解决的问题
我们经常都希望让一个线程简单地读取一些数据的内容。如果这些数据被一个互斥锁(比如SimpleSpinLock,SimpleWaitLock,SimpleHybridLock,AnotherHybridLock,Mutex或者Monitor)保护,那么当多个线程同时试图访问这些数据时,只有一个线程才会运行,其他所有线程都会阻塞。这会造成应用程序伸缩性和吞吐能力的急剧下降。如果所有线程都希望以只读方式访问数据,就根本没有必要阻塞它们;应该允许它们并发地访问数据。另一方面,如果一个线程希望修改数据,这个线程就需要对数据的独占式访问。
ReaderWriterLockSlim控制线程的逻辑

  • 一个线程向数据写入时,请求访问的其他所有线程都被阻塞。
  • 一个线程从数据读取时,请求读取的其他线程允许继续执行,但请求写入的线程仍被咀塞。
  • 向线程写入的线程结束后,要么解除一个写入线程(writer)的阻塞,使它能向数据写入,要么解除所有读取线程(reader)的阻塞,使它们能并发读取数据。如果没有线程被阻塞,锁就进入可以自由使用的状态,可供下一个reader或writer线程获取。
  • 从数据读取的所有线程结束后,一个writer线程被解除阻塞,使它能向数据写入。如果没有线程被阻塞,锁就进入可以自由使用的状态,可供下一个reader或writer线程获取。
public class ReaderWriterLockSlim : IDisposable { 
   public ReaderWriterLockSlim(LockRecursionPolicy recursionPolicy);  
   public void Dispose(); 
 
   public void    EnterReadLock(); 
   public Boolean TryEnterReadLock(Int32 millisecondsTimeout); 
   public void    ExitReadLock();  
 
   public void    EnterWriteLock(); 
   public Boolean TryEnterWriteLock(Int32 millisecondsTimeout);  
   public void    ExitWriteLock(); 
 
   // Most applications will never query any of these properties  
   public Boolean IsReadLockHeld              { get; } 
   public Boolean IsWriteLockHeld             { get; } 
   public Int32   CurrentReadCount            { get; } 
   public Int32   RecursiveReadCount          { get; } 
   public Int32   RecursiveWriteCount         { get; } 
   public Int32   WaitingReadCount            { get; } 
   public Int32   WaitingWriteCount           { get; } 
   public LockRecursionPolicy RecursionPolicy { get; } 
   // Members related to upgrading from a reader to a writer not shown 
}
internal sealed class Transaction : IDisposable { 
   private readonly ReaderWriterLockSlim m_lock =  
      new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion); 
   private DateTime m_timeOfLastTrans;  
 
   public void PerformTransaction() { 
      m_lock.EnterWriteLock(); 
      // This code has exclusive access to the data... 
      m_timeOfLastTrans = DateTime.Now; 
      m_lock.ExitWriteLock(); 
   } 
 
   public DateTime LastTransaction {  
      get {  
         m_lock.EnterReadLock(); 
         // This code has shared access to the data... 
         DateTime temp = m_timeOfLastTrans;  
         m_lock.ExitReadLock();  
         return temp;  
      } 
   } 
 
   public void Dispose() { m_lock.Dispose(); }  
}

ReaderWriterLockSlim的构造器允许传递一个LockRecurionsPolicy标志

public enum LockRecursionPolicy { NoRecursion, SupportsRecursion }

如果传递SupportsRecursion标志,锁就支持线程所有权和递归行为,这些行为对锁的性能有负面影响。建议总是向构造器传递LockRecursionPolicy.NoRecursion(就像本例),reader-writer锁支持线程所有权和递归的代价非常高昂,因为锁必须跟踪曾允许进入锁的所有reader线程,同时为每个线程都单独维护递归计数。

FCL还提供了一个ReaderWriterLock构造,它是在Microsoft NET Framework 1.0中引入的。这个构造存在许多问题,所以Microsoft在.NET Framework 3.5中引入了 ReaderWriterLockSlim构造。

30.3.4 OneManyLock类

创建了一个reader-writer构造,它的速度比FCL的ReaderWriterLockSlim类快。该类名为OneManyLock,因为它要么允许一个writer线程访问,要么允许多个reader线程访问。

public sealed class OneManyLock : IDisposable { 
   public OneManyLock(); 
   public void Dispose(); 
 
   public void Enter(Boolean exclusive); 
   public void Leave();  
}

假定当前有一个线程(reader)正在锁中进行读取操作,另一个线程(writer)想进入锁进行(独占的)写入操作。writer线程首先检查锁是否为Free,由于不为Free,所以线程会继续执行下一项检查。然而,在这个时候,reader线程可能离开了锁,而且在离开时发现RR和ww都是0。所以,线程会将锁的状态设为Free。这便造成了一个问题,因为writer线程已经执行过这个测试,并且走开了。简单地说,这里发生的事情是,reader线程背着writer线程改变了writer线程访问的状态。我需要解决这个问题,使锁能够正确工作。
为了解决这个问题,所有这些位操作都要使用29.3.4节"Interlocked Anything模式”描述的技术来执行。这个模式允许将任何操作转换成线程安全的原子操作。正是因为这个原因,才使得这个锁的速度是如此之快,而且其中维护的状态比其他reader-writer锁少。

当然,由于所有reader-writer锁都执行比互斥锁更多的逻辑,所以它们的性能可能要稍差一些。但在比较时不要忘记这样一个事实:reader-writer锁允许多个reader线程同时进入锁。

30.3.5 CountdownEvent类

System.Threading.CountdownEvent,这个构造使用了一个ManualResetEventSlim对象。这个构造阻塞一个线程,直到它的内部计数器变成0。从某种角度说,这个构造的行为和Semaphore的行为相反。(Semaphore是在计数为0时阻塞线程。)

public class CountdownEvent : IDisposable {  
   public CountdownEvent(Int32 initialCount); 
   public void Dispose(); 
   public void Reset(Int32 count);                // Set CurrentCount to count  
   public void AddCount(Int32 signalCount);       // Increments CurrentCount by signalCount  
   public Boolean TryAddCount(Int32 signalCount); // Increments CurrentCount by signalCount  
   public Boolean Signal(Int32 signalCount);      // Decrements CurrentCount by signameCount 
   public Boolean Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken); 
 
   public Int32      CurrentCount { get; } 
   public Boolean    IsSet        { get; }        // true if CurrentCount is 0  
   public WaitHandle WaitHandle   { get; } 
}
View Code

30.3.6 Barrier类

System.Threading.Barrier构造用于解决一个非常稀有的问题,平时一般用不上。Barrier控制的一系列线程需要并行工作,从而在一个算法的不同阶段推进。
例如:当CLR使用它的垃圾回收器(GC)的服务器版本时,GC算法为每个内核都创建一个线程。这些线程在不同应用程序线程的栈中向上移动,并发标记堆中的对象。每个线程完成了它自己的那一部分工作之后,必须停下来等待其他线程完成。所有线程都标记好对象后,线程就可以并发地压缩(compact)堆的不同部分。每个线程都完成了对它的那一部分的堆的压缩之后,线程必须阻塞以等待其他线程。所有线程都完成了对自己那一部分的堆的压缩之后,所有线程都要在应用程序的线程的栈中上行,对根进行修正,使之引用因为压缩而发生了移动的对象的新位置。只有在所有线程都完成这个工作之后,垃圾回收器的工作才算真正完成,应用程序的线程现在可以恢复执行了。

public class Barrier : IDisposable {  
   public Barrier(Int32 participantCount, Action<Barrier> postPhaseAction); 
   public void Dispose(); 
   public Int64 AddParticipants(Int32 participantCount);   // Adds participants 
   public void RemoveParticipants(Int32 participantCount); // Subtracts participants 
   public Boolean SignalAndWait(Int32 millisecondsTimeout, CancellationToken  
     cancellationToken); 
 
   public Int64 CurrentPhaseNumber    { get; }  // Indicates phase in process (starts at 0)  
   public Int32 ParticipantCount      { get; }  // Number of participants  
   public Int32 ParticipantsRemaining { get; }  // # of threads needing to call  
     SignalAndWait  
}
View Code

构造Barrier时要告诉它有多少个线程准备参与工作,还可传递一个Action<Barrier>委托来引用所有参与者完成一个阶段的工作后要调用的代码。可以调用AddParticipant和RemoveParticipant方法在Barrier中动态添加和删除参与线程。但在实际应用中,人们很少这样做。每个线程完成它的阶段性工作后,应调用SignalAndWait,告诉Barrier线程已经完成一个阶段的工作,而Barrier会阻塞线程(使用一个ManualResetEventSlim)。所有参与者都调用了SignalAndWait后,Barrier将调用指定的委托(由最后一个调用SignalAndWait的线程调用),然后解除正在等待的所有线程的阻塞,使它们开始下一阶段。

30.3.7 线程同步构造小结

我的建议是,代码尽量不要阻塞任何线程。执行异步计算或I/O操作时,将数据从一个线程交给另一个线程时,应避免多个线程同时访问数据。如果不能完全做到这一点,请尽量使用Volatile和Interlocked的方法,因为它们的速度很快,而且绝不阻塞线程。遗憾的是,这些方法只能操作简单类型。但可以像29.3.4节"Interlocked Anything模式”描述的那样在这些类型上执行丰富的操作。
主要在以下两种情况下阻塞线程。

  • 线程模型很简单
    阻塞线程虽会牺牲一些资源和性能,但可顺序地写应用程序代码,无需使用回调方法。不过,C#的异步方法功能现在提供了不阻塞线程的简化编程模型。
  • 线程有专门用途
    有的线程是特定任务专用的。最好的例子就是应用程序的主线程。如果应用程序的主线程没有阻塞,它最终就会返回,造成整个进程终止。其他例子还有应用程序的GUI线程。Windows要求一个窗口或控件总是由创建它的线程操作。因此,我们有时写代码阻塞一个GUI线程,直到其他某个操作完成。然后,GUI线程根据需要对窗口和控件进行更新。当然,阻塞GUI线程会造成应用程序挂起,使用户体验变差。

要避免阻塞线程,就不要刻意地为线程打上标签。为线程打上标签,其实是在告诫自己该线程不能做其他任何事情。但由于线程是如此昂贵,所以不能把它们专门用于某个目的。相反,应通过线程池将线程出租短暂时间。所以正确方式是一个线程池线程开始拼写检查,再改为语法检查,再代表一个客户端请求执行工作,以此类推。
如果一定要阻塞线程,为了同步在不同AppDomain或进程中运行的线程,请使用内核对象构造。要在一系列操作中原子性地操纵状态,请使用带有私有字段的Monitor类。另外,可以使用reader-writer锁代替Monitor,reader-writer锁通常比Monitor慢,但它们允许多个线程并发执行,这提升了总体性能,并将阻塞线程的机率降至最低。
此外,避免使用递归锁(尤其是递归的reader-writer锁),因为它们会损害性能。但Monitor是递归的,性能也不错。另外,不要在finally块中释放锁,因为进入和离开异常处理块会招致性能损失。如果在更改状态时抛出异常,状态就会损坏,操作这个状态的其他线程会出现不可预料的行为,并可能引入安全隐患。
最后,对于计算限制的工作,可以使用任务(参见第27.5节“任务”)避免使用大量线程同步构造。我喜欢的一个设计是,每个任务都关联一个或多个延续任务。某个操作完成后,这些任务将通过某个线程池线程继续执行。这比让一个线程阻塞并等待某个操作完成好得多。对于I/O限制的工作,调用各种XxxAsync方法将造成你的代码在I/O操作完成后继续;这其实类似于任务的延续任务。

30.4 著名的双检锁技术

双检锁(Double-Check Locking)是一个非常著名的技术,开发人员用它将单实例(singleton)对象的构造推迟到应用程序首次请求该对象时进行。这有时也称为延迟初始化(lazy initialization)。如果应用程序永远不请求对象,对象就永远不会构造,从而节省了时间和内存。但当多个线程同时请求单实例对象时就可能出问题。这个时候必须使用一些线程同步机制确保单实例对象只被构造一次。

internal sealed class Singleton {  
   // s_lock is required for thread safety and having this object assumes that creating   
   // the singleton object is more expensive than creating a System.Object object and that   
   // creating the singleton object may not be necessary at all. Otherwise, it is more    
   // efficient and easier to just create the singleton object in a class constructor 
   private static readonly Object s_lock = new Object();  
  
   // This field will refer to the one Singleton object 
   private static Singleton s_value = null;   
     
   // Private constructor prevents any code outside this class from creating an instance  
   private Singleton() {  
      // Code to initialize the one Singleton object goes here... 
   } 
  
   // Public, static method that returns the Singleton object (creating it if necessary)  
   public static Singleton GetSingleton() {  
         // If the Singleton was already created, just return it (this is fast)  
      if (s_value != null) return s_value; 
 
      Monitor.Enter(s_lock);  // Not created, let 1 thread create it  
      if (s_value == null) {   
         // Still not created, create it 
         Singleton temp = new Singleton(); 
 
         // Save the reference in s_value (see discussion for details) 
         Volatile.Write(ref s_value, temp);  
      } 
      Monitor.Exit(s_lock); 
  
      // Return a reference to the one Singleton object   
      return s_value;  
   }  
}

双检锁技术背后的思路在于,对GetSingleton方法的一个调用可以快速地检查s-value字段,判断对象是否创建。如果是,方法就返回对它的引用。这里的妙处在于,如果对象已经构造好,就不需要线程同步;应用程序会运行得非常快。另一方面,如果调用GetSingleton方法的第一个线程发现对象还没有创建,就会获取一个线程同步锁来确保只有一个线程构造单实例对象。这意味着只有线程第一次查询单实例对象时,才会出现性能上的损失。

在CLR中,对任何锁方法的调用都构成了一个完整的内存栅栏,在栅栏之前写入的任何变量都必须在栅栏之前完成:在栅栏之后的任何变量读取都必须在栅栏之后开始。对于GetSingleton方法,这意味着s-value字段的值必须在调用了Monitor.Enter之后重新读取:调用前缓存到寄存器中的东西作不了数。

让编译器生成代码为一个Singleton分配内存,调用构造器来初始化字段,再将引用赋给s_value字段。使一个值对其他线程可见称为发布(publishing)。但那只是你一厢情愿的想法,编译器可能这样做:为Singleton分配内存,将引用发布到(赋给)s_value,再调用构造器。从单线程的角度出发,像这样改变顺序是无关紧要的。但在将引用发布给s_value之后,并在调用构造器之前,如果另一个线程调用了GetSingleton方法,那么会发生什么?
这个线程会发现s_value不为null,所以会开始使用Singleton对象,但对象的构造器还没有结束执行呢!这是一个很难追踪的bug,尤其是它完全是由于计时而造成的。
对Volatile.Write的调用修正了这个问题。它保证temp中的引用只有在构造器结束执行之后,才发布到s_value中。解决这个问题的另一个办法是使用C#的volatile关键字来标记s_value字段。这使向s_value的写入变得具有“易变性”。同样,构造器必须在写入发生前结束运行。但遗憾的是,这同时会使所有读取操作具有“易变性”,这是完全没必要的。因此,使用volatile关键字,会使性能无谓地受到损害。

internal sealed class Singleton {  
   private static Singleton s_value = new Singleton();    
     
   // Private constructor prevents any code outside this class from creating an instance  
   private Singleton() { 
      // Code to initialize the one Singleton object goes here... 
   }  
 
   // Public, static method that returns the Singleton object (creating it if necessary)  
   public static Singleton GetSingleton() { return s_value; }  
}

由于代码首次访问类的成员时,CLR会自动调用类型的类构造器,所以首次有一个线程查询Singleton的GetSingleton方法时,CLR就会自动调用类构造器,从而创建一个对象实例。此外,CLR已保证了对类构造器的调用是线程安全的。我已在8.3节“类型构造器”对此进行了解释。

这种方式的缺点在于,首次访问类的任向成员都会调用类型构造器。所以,如果Singleton类型定义了其他静态成员,就会在访问其他任何静态成员时创建Singleton对象。有人通过定义嵌套类来解决这个问题。

internal sealed class Singleton {  
   private static Singleton s_value = null;  
 
   // Private constructor prevents any code outside this class from creating an instance  
   private Singleton() { 
      // Code to initialize the one Singleton object goes here... 
   } 
 
   // Public, static method that returns the Singleton object (creating it if necessary)  
   public static Singleton GetSingleton() {  
      if (s_value != null) return s_value; 
       // Create a new Singleton and root it if another thread didn't do it first 
      Singleton temp = new Singleton(); 
      Interlocked.CompareExchange(ref s_value, temp, null); 
 
      // If this thread lost, then the second Singleton object gets GC'd 
 
      return s_value; // Return reference to the single object 
   } 
}

如果多个线程同时调用GetSingleton,这个版本可能创建两个(或更多)Singleton对象。然而,对Interlocked.CompareExchange的调用确保只有一个引用才会发布到s _value字段中。没有通过这个字段固定下来的任何对象 会在以后被垃圾回收。由在大多数应用程序都很少发生多个线程同时调用GetSingleton的情况,所以不太可能同时创建多个Singleton对象。
虽然可能创建多个Singleton对象,但上述代码有多方面的优势。首先,它的速度非常快。其次,它永不阻塞线程。相反,如果一个线程池线程在一个Monitor或者其他任何内核模式的线程同步构造上阻塞,线程池就会创建另一个线程来保持CPU的“饱和”。因此,会分配并初始化更多的内存,而且所有DLL都会收到一个线程连接通知。使用CompareExchange则永远不会发生这种情况。当然,只有在构造器没有副作用的时候才能使用这个技术。

FCL有两个类型封装了本节描述的模式。

泛型System.Lazy类

public class Lazy<T> { 
   public Lazy(Func<T> valueFactory, LazyThreadSafetyMode mode); 
   public Boolean IsValueCreated { get; }  
   public T Value { get; }  
}
public static void Main() { 
   // Create a lazy­initialization wrapper around getting the DateTime 
   Lazy<String> s = new Lazy<String>(() => DateTime.Now.ToLongTimeString(), true); 
 
   Console.WriteLine(s.IsValueCreated);   // Returns false because Value not queried yet  
   Console.WriteLine(s.Value);            // The delegate is invoked now 
   Console.WriteLine(s.IsValueCreated);   // Returns true because Value was queried  
   Thread.Sleep(10000);                   // Wait 10 seconds and display the time again 
   Console.WriteLine(s.Value);            // The delegate is NOT invoked now; same result 
}
public enum LazyThreadSafetyMode {  
   None,                      // No thread­safety support at all (good for GUI apps)  
   ExecutionAndPublication    // Uses the double ­check locking technique  
   PublicationOnly,           // Uses the Interlocked.CompareExchange technique 
}

 System.Threading.LazyInitializer类

内存有限时可能不想创建Lazy类的实例。这时可调用System.Threading.LazyInitializer类的静态方法。

public static class LazyInitializer { 
   // These two methods use Interlocked.CompareExchange internally:   
   public static T EnsureInitialized<T>(ref T target) where T: class; 
   public static T EnsureInitialized<T>(ref T target, Func<T> valueFactory) where T: class;  
 
   // These two methods pass the syncLock to Monitor's Enter and Exit methods internally  
   public static T EnsureInitialized<T>(ref T target, ref Boolean initialized,  
      ref Object syncLock); 
   public static T EnsureInitialized<T>(ref T target, ref Boolean initialized,  
      ref Object syncLock, Func<T> valueFactory); 
}

为EnsureInitialized方法的syncLock参数显式指定同步对象,可以用同一个锁保护多个初始化函数和字段。

public static void Main() { 
   String name = null;   
   // Because name is null, the delegate runs and initializes name  
   LazyInitializer.EnsureInitialized(ref name, () => "Jeffrey");    
   Console.WriteLine(name);   // Displays "Jeffrey"  
 
   // Because name is not null, the delegate does not run; name doesn’t change  
   LazyInitializer.EnsureInitialized(ref name, () => "Richter"); 
   Console.WriteLine(name);   // Also displays "Jeffrey"  
}

30.5 条件变量模式

条件变量(condition variable)模式
假定一个线程希望在一个复合条件为true时执行一些代码。一个选项是让线程连续“自旋”,反复测试条件。但这会浪费CPU时间,也不可能对构成复合条件的多个变量进行原子性的测试。幸好,有一个模式允许线程根据一个复合条件来同步它们的操作,而且不会浪费资源。

internal sealed class ConditionVariablePattern {  
   private readonly Object m_lock = new Object(); 
   private Boolean m_condition = false; 
 
   public void Thread1() {  
      Monitor.Enter(m_lock);        // Acquire a mutual­exclusive lock 
 
      // While under the lock, test the complex condition "atomically" 
      while (!m_condition) {  
         // If condition is not met, wait for another thread to change the condition 
         Monitor.Wait(m_lock);      // Temporarily release lock so other threads can get it  
      } 
 
      // The condition was met, process the data...  
 
      Monitor.Exit(m_lock);         // Permanently release lock  
   } 
 
   public void Thread2() {  
      Monitor.Enter(m_lock);        // Acquire a mutual­exclusive lock 
 
      // Process data and modify the condition... 
      m_condition = true; 
 
      // Monitor.Pulse(m_lock);     // Wakes one waiter AFTER lock is released  
      Monitor.PulseAll(m_lock);     // Wakes all waiters AFTER lock is released 
 
      Monitor.Exit(m_lock);         // Release lock  
   } 
}

在上述代码中,执行Thread1方法的线程进入一个互斥锁,然后对一个条件进行测试。在这里,我只是检查一个Boolean字段,但它可以是任意复合条件。例如,可以检查是不是三月的一个星期二,同时一个特定的集合对象是否包含10个元素。如果条件为false,你希望线程在条件上“自旋”。但自旋会浪费CPU时间,所以线程不是自旋,而是调用wait.Wait释放锁,使另一个线程能获得它并阻塞调用线程。
Thread2方法展示了第二个线程执行的代码。它调用Enter来获取锁的所有权,处理一些数据,造成一些状态的改变,再调用Pulse或PulseAll,从而解除一个线程因为调用wait而进入的阻塞状态。注意,Pulse只解除等待最久的线程(如果有的话)的阻塞,而PulseAll解除所有正在等待的线程(如果有的话)的阻塞。但所有未阻塞的线程还没有醒来。执行Thread2的线程必须调用Monitor.Exit,允许锁由另一个线程拥有。另外,如果调用的是PulseAll,其他线程不会同时解除阻塞。调用Wait的线程解除阻塞后,它成为锁的所有者。由于这是一个互斥锁,所以一次只能有一个线程拥有它。其他线程只有在锁的所有者调用了Wait或Exit之后才能得到它。
执行Threadl的线程醒来时,它进行下一次循环迭代,再次对条件进行测试。如果条件仍为false,它就再次调用Wait。如果条件为true,它就处理数据,并最终调用Exit,这样就会将锁释放,使其他线程能得到它。这个模式的妙处在于,可以使用简单的同步逻辑(只是一个锁)来测试构成一个复合条件的几个变量,而且多个正在等待的线程可以全部解除阻塞,而不会造成任何逻辑错误。唯一的缺点就是解除线程的阻塞可能浪费一些CPU时间。
下面展示了一个线程安全的队列,它允许多个线程在其中对数据项(item)进行入队和出队操作。注意,除非有了一个可供处理的数据项,否则试图出队一个数据项的线程会一直阻塞。

internal sealed class SynchronizedQueue<T> { 
   private readonly Object m_lock = new Object(); 
   private readonly Queue<T> m_queue = new Queue<T>(); 
 
   public void Enqueue(T item) { 
      Monitor.Enter(m_lock);  
     
      // After enqueuing an item, wake up any/all waiters 
      m_queue.Enqueue(item);  
      Monitor.PulseAll(m_lock);  
 
      Monitor.Exit(m_lock); 
   } 
 
   public T Dequeue() {  
      Monitor.Enter(m_lock);  
 
      // Loop while the queue is empty (the condition) 
      while (m_queue.Count == 0)   
         Monitor.Wait(m_lock); 
 
      // Dequeue an item from the queue and return it for processing  
      T item = m_queue.Dequeue();  
      Monitor.Exit(m_lock); 
      return item;  
   } 
}

30.6 异步的同步构造

假定客户端向网站发出请求。客户端请求到达时,一个线程池线程开始处理客户端请求。假定这个客户端想以线程安全的方式修改服务器中的数据,所以它请求一个reader-writen锁来进行写入(这使线程成为一个writer线程),假定这个锁被长时间占有。在锁被占有期间,另一个客户端请求到达了,所以线程池为这个请求创建新线程。然后,线程阻塞,尝试获取reader-writer锁来进行读取(这使线程成为一个reader线程),事实上,随着越来越多的客户端请求到达,线程池会创建越来越多的线程,所有这些线程都傻傻地在锁上面阻塞。服务器把它的所有时间都花在创建线程上面,而目的仅仅是让它们停止运行!,这样的服务器完全没有伸缩性可言。
更糟的是,当writer线程释放锁时,所有reader线程都同时解除阻塞并开始执行。现在,又变成了大量线程试图在相对数量很少的CPU上运行。所以,Windows开始在线程之间不停地进行上下文切换。由于上下文切换产生了大量开销,所以真正的工作反正没有得到很快的处理。

观察本章介绍的所有构造,你会发现这些构造想要解决的许多问题其实最好都是用第27章讨论的Task类来完成。
Task任务具有下述许多优势:

  • 任务使用的内存比线程少得多,创建和销毁所需的时间也少得多。
  • 线程池根据可用CPU数量自动伸缩任务规模。
  • 每个任务完成一个阶段后,运行任务的线程回到线程池,在那里能接受新任务。
  • 线程池是站在整个进程的高度观察任务。所以,它能更好地调度这些任务,减少进程中的线程数,并减少上下文切换。

锁很流行,但长时间拥有会带来巨大的伸缩性问题。如果代码能通过异步的同步构造指出它想要一个锁,那么会非常有用。在这种情况下,如果线程得不到锁,可直接返回并执行其他工作,而不必在那里傻傻地阻塞。以后当锁可用时,代码可恢复执行并访问锁所保护的资源。

SemaphoresSlim类通过WaitAsync方法实现了这个思路,下面是该方法的最复杂的重载版本的签名。

public Task<Boolean> WaitAsync(Int32 millisecondsTimeout, CancellationToken cancellationToken);

可用它异步地同步对一个资源的访问(不阻塞任何线程)。

private static async Task AccessResourceViaAsyncSynchronization(SemaphoreSlim asyncLock) { 
   // TODO: Execute whatever code you want here... 
 
   await asyncLock.WaitAsync();     // Request exclusive access to a resource via its lock 
   // When we get here, we know that no other thread is accessing the resource  
   // TODO: Access the resource (exclusively)...  
 
   // When done accessing resource, relinquish lock so other code can access the resource 
   asyncLock.Release();  
 
   // TODO: Execute whatever code you want here... 
}

SemaphoreSlim的WaitAsync方法很有用,但它提供的是信号量语义。一般创建最大计数为1的SemaphoreSlim,从而对SemaphoreSlim保护的资源进行互斥访问。所以,这和使用Monitor时的行为相似,只是Semaphoreslim不支持线程所有权和递归语义(这是好事)。
但reader-writer语义呢?.NET Framework提供了ConcurrentExclusiveSchedulerPair类。

public class ConcurrentExclusiveSchedulerPair { 
   public ConcurrentExclusiveSchedulerPair(); 
 
   public TaskScheduler ExclusiveScheduler  { get; } 
   public TaskScheduler ConcurrentScheduler { get; } 
 
   // Other methods not shown... 
}

这个类的实例带有两个TaskScheduler对象,它们在调度任务时负责提供reader/writer语义。
只要当前没有运行使用ConcurrentScheduler调度的任务,使用ExclusiveScheduler调度的任何任务将独占式地运行(一次只能运行一个)。另外,只要当前没有运行使用Exclusivescheduler调度的任务,使用ConcurrentScheduler调度的任务就可同时运行(一次运行多个)。以下代码演示了该类的使用。

private static void ConcurrentExclusiveSchedulerDemo() {  
   var cesp = new ConcurrentExclusiveSchedulerPair();  
   var tfExclusive = new TaskFactory(cesp.ExclusiveScheduler); 
   var tfConcurrent = new TaskFactory(cesp.ConcurrentScheduler); 
 
   for (Int32 operation = 0; operation < 5; operation++) {  
      var exclusive = operation < 2; // For demo, I make 2 exclusive & 3 concurrent  
 
      (exclusive ? tfExclusive : tfConcurrent).StartNew(() => {  
         Console.WriteLine("{0} access", exclusive ? "exclusive" : "concurrent");  
         // TODO: Do exclusive write or concurrent read computation here... 
      }); 
   }   
}

遗憾的是,.NET Framework没有提供具有reader-writer语义的异步锁。但我构建了这样的一个类,称为AsyncOneManyLock。它的用法和SemaphoreSlim一样。下面是一个例子。

private static async Task AccessResourceViaAsyncSynchronization(AsyncOneManyLock asyncLock) {  
   // TODO: Execute whatever code you want here... 
 
   // Pass OneManyMode.Exclusive or OneManyMode.Shared for wanted concurrent access  
   await asyncLock.AcquireAsync(OneManyMode.Shared); // Request shared access 
   // When we get here, no threads are writing to the resource; other threads may be reading 
   // TODO: Read from the resource... 
 
   // When done accessing resource, relinquish lock so other code can access the resource 
   asyncLock.Release();  
 
   // TODO: Execute whatever code you want here... 
}

下面展示了AsyncOneManyLock类。

public enum OneManyMode { Exclusive, Shared } 
 
public sealed class AsyncOneManyLock {  
   #region Lock code 
   private SpinLock m_lock = new SpinLock(true);   // Don't use readonly with a SpinLock  
   private void Lock() { Boolean taken = false; m_lock.Enter(ref taken); } 
   private void Unlock() { m_lock.Exit(); }  
   #endregion  
 
   #region Lock state and helper methods 
   private Int32 m_state = 0; 
   private Boolean IsFree { get { return m_state == 0; } }  
   private Boolean IsOwnedByWriter { get { return m_state ==  ­1; } }  
   private Boolean IsOwnedByReaders { get { return m_state > 0; } } 
   private Int32 AddReaders(Int32 count) { return m_state += count; } 
   private Int32 SubtractReader() { return  ­­m_state; } 
   private void MakeWriter() { m_state =  ­1; } 
   private void MakeFree() { m_state = 0; }  
   #endregion  
 
   // For the no ­contention case to improve performance and reduce memory consumption  
   private readonly Task m_noContentionAccessGranter;  
 
   // Each waiting writers wakes up via their own TaskCompletionSource queued here 
   private readonly Queue<TaskCompletionSource<Object>> m_qWaitingWriters = 
      new Queue<TaskCompletionSource<Object>>();  
 
   // All waiting readers wake up by signaling a single TaskCompletionSource  
   private TaskCompletionSource<Object> m_waitingReadersSignal = 
      new TaskCompletionSource<Object>();  
   private Int32 m_numWaitingReaders = 0;  
 
 
   public AsyncOneManyLock() { 
      m_noContentionAccessGranter = Task.FromResult<Object>(null);  
   } 
 
   public Task WaitAsync(OneManyMode mode) { 
      Task accressGranter = m_noContentionAccessGranter; // Assume no contention 
 
      Lock();  
      switch (mode) {  
         case OneManyMode.Exclusive:  
            if (IsFree) { 
               MakeWriter();  // No contention  
            } else { 
               // Contention: Queue new writer task & return it so writer waits 
               var tcs = new TaskCompletionSource<Object>(); 
               m_qWaitingWriters.Enqueue(tcs);  
               accressGranter = tcs.Task;  
            }  
            break;  
 
         case OneManyMode.Shared:  
            if (IsFree || (IsOwnedByReaders && m_qWaitingWriters.Count == 0)) { 
               AddReaders(1); // No contention  
            } else { // Contention 
               // Contention: Increment waiting readers & return reader task so reader waits 
               m_numWaitingReaders++; 
               accressGranter = m_waitingReadersSignal.Task.ContinueWith(t => t.Result);  
            }  
            break;  
      } 
      Unlock(); 
 
      return accressGranter;  
   } 
   
   public void Release() {  
      TaskCompletionSource<Object> accessGranter = null;   // Assume no code is released  
 
      Lock();  
      if (IsOwnedByWriter) MakeFree(); // The writer left 
      else SubtractReader();           // A reader left 
 
      if (IsFree) { 
         // If free, wake 1 waiting writer or all waiting readers 
         if (m_qWaitingWriters.Count > 0) {  
            MakeWriter(); 
            accessGranter = m_qWaitingWriters.Dequeue();  
         } else if (m_numWaitingReaders > 0) {  
            AddReaders(m_numWaitingReaders); 
            m_numWaitingReaders = 0;  
            accessGranter = m_waitingReadersSignal;  
 
            // Create a new TCS for future readers that need to wait  
            m_waitingReadersSignal = new TaskCompletionSource<Object>(); 
         } 
      } 
      Unlock(); 
 
      // Wake the writer/reader outside the lock to reduce  
      // chance of contention improving performance  
      if (accessGranter != null) accessGranter.SetResult(null);  
   } 
}
View Code

如同我说过的一样,上述代码永远不会阻塞线程。原因是我在内部没有使用任何内核构造。这里确实使用了一个SpinLock,它在内部使用了用户模式的构造。但第29章讨论自旋锁的时候说过,只有执行时间很短的代码段才可以用自旋锁来保护。查看我的WaitAsyne方法,会发现我用锁保护的只是一些整数计算和比较,以及构造一个TaskCompletionSource并把它添加到队列的动作。这花不了多少时间,所以能保证锁只是短时间被占有。
类似地,查看我的Release方法,会发现做的事情不外乎一些整数计算、一个比较以及将一个TaskCompletionSource出队或者构造一个TaskCompletionSource。

30.7并发集合类

FCL自带4个线程安全的集合类,全部在System.Collections.Concurrent命名空间中定义。它们是ConcurrentQueueConcurrentStackConcurrentDictionaryConcurrentBag.
所有这些集合类都是“非阻塞”的。换言之,如果一个线程试图提取一个不存在的元素(数据项),线程会立即返回:线程不会阻塞在那里,等着一个元素的出现。正是由于这个原因所以如果获取了一个数据项,像TryDequeue,TryPop,TryTake和TryGetValue这样的方法全都返回true;否则返回false.
一个集合“非阻塞”,并不意味着它就不需要锁了。ConcurrentDictionary类在内部使用了Monitor。但是,对集合中的项进行操作时,锁只被占有极短的时间。ConcurrentQueue和ConcurrentStack确实不需要锁:它们两个在内部都使用Interlocked的方法来操纵集合。一个ConcurrentBag对象(一个bag)由大量迷你集合对象构成,每个线程一个。线程将一个项添加到bag中时,就用Interlocked的方法将这个项添加到调用线程的迷你集合中。一个线程试图从bag中提取一个元素时,bag就检查调用线程的迷你集合,试图从中取出数据项。如果数据项在那里,就用一个Interlocked方法提取这个项。如果不在,就在内部获取一个Monitor,以便从另一个线程的迷你集合提取一个项。这称为一个线程从另一个线程“窃取”一个数据项。
注意,所有并发集合类都提供了GetEnumerator方法,它一般用于C#的foreach语句,但也可用于LINQ,对于ConcurrentStack,ConcurrentQueue和ConcurrentBag类,GetEnumerator方法获取集合内容的一个“快照”,并从这个快照中返回元素;实际集合的内容可能在使用快照枚举时发生改变。ConcurrentDictionary的GetEnumerator方法不获取它的内容的快照。因此,在枚举字典期间,字典的内容可能改变;这一点务必注意。还要注意的是,Count属性返回的是查询时集合中的元素数量。如果其他线程同时正在集合中增删元素,这个计数可能马上就变得不正确了。
ConcurrentStack,ConcurrentQueue和ConcurrentBag这三个并发集合类都实现了IProducerConsumerCollection接口。

public interface IProducerConsumerCollection<T> : IEnumerable<T>, ICollection, IEnumerable { 
   Boolean TryAdd(T item);  
   Boolean TryTake(out T item);  
   T[] ToArray(); 
   void CopyTo(T[] array, Int32 index); 
}

实现了这个接口的任何类都能转变成一个阻塞集合。如果集合已满,那么负责生产(添加)数据项的线程会阻塞:如果集合已空,那么负责消费(移除)数据项的线程会阻塞。当然,我会尽量不使用这种阻塞集合,因为它们生命的全部意义就是阻塞线程。要将非阻塞的集合转变成阻塞集合,需要构造一个System.Collections.Concurrent.BlockingCollection类,向它的构造器传递对非阻塞集合的引用。

public class BlockingCollection<T> : IEnumerable<T>, ICollection, IEnumerable, IDisposable { 
   public BlockingCollection(IProducerConsumerCollection<T> collection,  
      Int32 boundedCapacity); 
 
   public void Add(T item); 
   public Boolean TryAdd(T item, Int32 msTimeout, CancellationToken cancellationToken); 
   public void CompleteAdding(); 
 
   public T Take(); 
   public Boolean TryTake(out T item, Int32 msTimeout, CancellationToken cancellationToken); 
 
   public Int32   BoundedCapacity   { get; } 
   public Int32   Count             { get; } 
   public Boolean IsAddingCompleted { get; }  // true if CompleteAdding is called  
   public Boolean IsCompleted       { get; }  // true if IsAddingComplete is true and Count==0 
 
   public IEnumerable<T> GetConsumingEnumerable(CancellationToken cancellationToken); 
   public void CopyTo(T[] array, int index); 
   public T[] ToArray(); 
   public void Dispose(); 
}

构造一个BlockingCollection时,boundedCapacity参数指出你想在集合中最多容纳多少个数据项。在基础集合已满的时候,如果一个线程调用Add,生产线程就会阻塞。如果愿意,生产线程可调用TryAdd,传递一个超时值(以毫秒为单位)和/或一个CancellationToken,使线程一直阻塞,直到数据项成功添加、超时到期或者CancellationToken被取消(对CancellationToken类的讨论请参见第27章)。

BlockingCollection类实现了IDisposable接口。调用Dispose时,这个Dispose会调用基础集合的Dispose。它还会对类内部用于阻塞生产者和消费者的两个Semaphoreslim对象进行清理。
生产者不再向集合添加更多的项时,生产者应调用CompleteAdding方法。这会向消费者发出信号,让它们知道不会再生产更多的项了。具体地说,这会造成正在使用GetConsumingEnumerable的一个foreach循环终止。

public static void Main() { 
   var bl = new BlockingCollection<Int32>(new ConcurrentQueue<Int32>()); 
 
   // A thread pool thread will do the consuming  
   ThreadPool.QueueUserWorkItem(ConsumeItems, bl); 
 
   // Add 5 items to the collection 
   for (Int32 item = 0; item < 5; item++) {  
      Console.WriteLine("Producing: " + item);  
      bl.Add(item); 
   } 
 
   // Tell the consuming thread(s) that no more items will be added to the collection 
   bl.CompleteAdding();  
 
   Console.ReadLine();  // For testing purposes 
} 
 
private static void ConsumeItems(Object o) { 
   var bl = (BlockingCollection<Int32>) o; 
 
   // Block until an item shows up, then process it  
   foreach (var item in bl.GetConsumingEnumerable()) { 
      Console.WriteLine("Consuming: " + item);  
   } 
 
   // The collection is empty and no more items are going into it 
   Console.WriteLine("All items have been consumed");  
}

BlockingCollection类还提供了静态AddToAny,TryAddToAny,TakeFromAny和TryTakeFromAny方法。所有这些方法都获取一个BlockingCollection<T11,以及一个数据项、一个超时值以及一个CancellationToken。(Try)AddToAny方法遍历数组中的所有集合,直到发现因为容量还没有达到(还没有满),而能够接受数据项的一个集合。(Try)TakeFromAny方法则遍历数组中的所有集合,直到发现一个能从中移除一个数据项的集合。

posted @ 2019-11-16 17:30  FH1004322  阅读(197)  评论(0)    收藏  举报