CLR Via C# 3rd 阅读摘要 -- Chapter 29 – Hybrid Thread Synchronization Constructs
A Simple Hybrid Lock
- 混合结构在没有竞争的情况下,提供了原生用户模式同步结构的性能优点。同时在有竞争的情况下,提供了核心模式同步结构不浪费CPU资源的优点;
- SimpleHybridLock:
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 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 is waiting. There is contention, block this thread 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 blocked, just return // Other threads are blocked, wake 1 of them m_waiterLock.Set(); // Bad performance hit here. } }
Spinning, Thread Ownership, and Recursion
- AnotherHybridLock:
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) { // Other threads are blocked and this thread must block too m_waiterLock.WaitOne(); // Wait for the lock; preformance 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; // If no other threads are blocked, just return if (Interlocked.Decrement(ref m_waiters) == 0) return; // Other threads are bloced, wake 1 of them m_waiterLock.Set(); // Bad performance hit here } }
- 性能比较参考:
Incrementing x: 8 Fastest Incrementing x in Mutex: 50 6x slower Incrementing x in SimpleSpinLock: 210 26x slower Incrementing x in SimpleHybridLock: 211 26x slower (similar to SimpleSpinLock) Incrementing x in AnotherHybridLock: 415 52x slower (due to ownership/recursion) Incrementing x in SimpleWaitLock: 17,615 2,201x slower
A Potpourri of Hybrid Constructs
- System.Threading.ManualResetEventSlim,System.Threading.SemaphoreSlim:跟对应的ManualResetEvent, Semaphore很相似,不同点:直到第一次竞争出现,它们将轮转在用户模式并推迟创建内核模式同步结构;另外,Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)方法可以传入超时参数和CancellationToken;
- Monitor(关键字lock,在JIT时会使用Monitor),一个常用的混合模式同步结构类,提供了支持轮转、线程所有权、递归计数的互斥锁;
- 每个托管堆上的对象都有一个叫做“同步块(Sync Block)”的数据结构跟它相关联,该数据结构具备字段:内核对象、所属线程ID、递归计数、等待线程数量。
- 当CLR初始化时,会分配一个同步块数组。每个托管堆上的对象,都有两个开销字段:类型对象指针(Type Object Pointer)、同步块索引(Sync Block Index) -- 关联到系统同步块数组的索引(-1:不关联任何同步块);
- 当Mointer.Enter方法被调用后,CLR会在数组中找到一个空闲的同步块,然后设置对象的同步块索引指向同步块。当Monitor.Exit被调用后,CLR会检查是否有其他线程在等待使用对象的同步块。如果没有,同步块被释放,对象的Sync Block Index被设置成-1;
- 同步块可以被关联到类型对象,类型对象的引用可以传递给Monitor的方法;
- Monitor.Enter(this), .Exit(this),这里会出现一个比较晦涩的BUG,this所代表的对象在外部可能会被作为锁,这样就不能正常工作,所以总是使用一个私有锁代替,
private readonly Object m_lock = new Object(); ... Monitor.Enter(m_lock); ... Monitor.Exit(m_lock);
; - Monitor是一个静态类,所以存在一些问题需要注意:
- 如果对象的类型指向System.MarshalByRefObject类型的子类,一个变量可以引用到一个代理对象。当通过传递代理对象的引用调用Monitor的方法时,锁住的是代理对象,而不是实际对象;
- 如果一个线程调用Monitor.Enter时,传递一个以及域中立加载的类型对象的引用,线程给进程中所有AppDomain中的该类型上锁。这是一个BUG,所以绝不使用类型对象的引用调用Monitor的方法;
- 因为Monitor的方法使用Object做参数,所以传递一个值类型会引起装箱操作。这样锁住的是装箱之后的对象,所以一切同步都白搭;
- 使用[MethodImpl(MethodImplOptions.Synchronized)]属性到一个方法上,会引起JIT编译器使用Monitor.Enter和Monitor.Exit包转在本地代码上,如果该方法是个实例方法,那么this就是Monitor方法的参数,锁住的是隐式的公共锁(可能是个噩梦)。如果该方法是静态方法,那么类型的类型对象就是Monitor方法的参数,那就噩梦连绵了。所以,绝不要使用在方法上使用[MethodImpl(MethodImplOptions.Synchronized)]属性
- 当调用类型的类型构造器(静态构造器),CLR在类型的类型对象上加锁以确定只有一个线程初始化该类型对象和它的静态字段。建议:尽可能的避免类型构造器,或者保证它短小精干。
- lock会引入Monitor和try/finally块,而JIT不会内联编译带有try块的方法,这会导致降低性能。所以一般不推荐使用lock关键字;
- ReaderWriterLockSlim, OneManyLock, CountdownEvent, Barrier, ...
- 建议写代码时,别阻塞所有的线程,尽可能短的持有锁;
- 读写锁通常比Monitor要慢,但是允许多个读线程并发执行,改善了整体性能,同时最小化了线程阻塞的可能;
- 避免使用递归锁(特别是读写递归锁),因为太糟蹋性能了;
- 避免在finally中释放锁,因为进入和离开异常处理块会有性能负担。尤其是在改变状态时出现了异常,那么会导致无法预知的行为,并出现安全漏洞;
- 面向计算方向的工作,还是使用task;
- 面向I/O方向的工作,采用APM在I/O操作完成后调用回调方法;
- 可以使用SpinLock代替Monitor因为SpinLock稍微快一点,但是SpinLock可能会浪费CPU。Monitor实际上也挺快,因为使用本地代码实现的,而不是托管代码;
The Famous Double-Check Locking Technique
- 延迟初始化,单例对象知道应用程序使用它才开始初始化,节约时间和内存。潜在问题会出现在当多个线程同时首次访问该对象时;
- The "Double-Checked Locking is Broken" Declaration
- 不幸的Java两次检查锁单例代码,如果编译器进行优化或者共享内存的多处理器环境下,下面的代码不会正常工作,原因有一大箩筐。
// Broken multithreaded version // "Double-Checked Locking" idiom class Foo { private Helper helper = null; public Helper getHelper() { if (helper == null) synchronized(this) { if (helper == null) helper = new Helper(); } return helper; } // other functions and members... }
简单的办法,使用静态字段:class HelperSingleton { static Helper singleton = new Helper(); }
或者,使用JDK5的新语法,volatile关键字:// Works with acquire/release semantics for volatile // Broken under current semantics for volatile class Foo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) helper = new Helper(); } } return helper; } }
- CLR支持Double-Checked Locking,C#版:
internal sealed class Singleton { private static readonly Object s_lock = new Object(); private static Singleton s_value = null; private Singleton() { } public static Singleton GetSingleton() { if (s_value != null) return s_value; Monitor.Enter(s_lock); if (s_value == null) { Singleton temp = new Singleton(); Interlocked.Exchange(ref s_value, temp); } Monitor.Exit(s_lock); return s_value; } }
- 其实,这才是最实用的单例模式:
internal sealed class Singleton { private static Singleton s_value = new Singleton(); private Singleton() { } public static Singleton GetSingleton() { return s_value; } }
因为CLR在代码第一次尝试访问类的成员时,会自动调用类型的类构造器(class initializer, type initializer, static initializer)。线程第一次调用Singleton.GetSingletonk静态方法时,CLR自动调用类构造器(注意Before-Field-Init语义),创建出一个该对象的实例。并且:CLR保证调用类型构造器是线程安全的。 - 上面的代码有一点点遗憾,如果Singleton中有其他静态成员,如果任何一个被访问时,Singleton对象就会被创建,这可能并不是所想要的。那么下面的代码简单实用高效可靠:
internal sealed class Singleton { private static Singleton s_value = null; private Singleton() { } public static Singleton GetSingleton() { if (s_value != null) return s_value; Singleton temp = new Singleton(); Interlocked.CompareExchange(ref s_value, temp, null); return s_value; } }
- System.Lazy
,System.Threading.LazyInitializer的静态方法,延迟加载和延迟初始化。
The Condition Variable Pattern
- 当满足一组复杂的条件时,线程执行特定的代码,否则不停轮转检测条件,这通常会浪费CPU时间,条件变量模式(condition variable pattern)可以基于复杂条件有效的进行同步操作;
- 简单的条件变量模式例子:
internal sealed class ConditionVariablePattern { private readonly Object m_lock = new Object(); private Boolean m_condition = false; public void Thread1() { Monitor.Enter(m_lock); while (!m_condition) { Monitor.Wait(m_lock); } Monitor.Exit(m_lock); } public void Thread2() { Monitor.Enter(m_lock); m_condition = true; Monitor.PulseAll(m_lock); Monitor.Exit(m_lock); } }
- 线程安全的队列:
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); m_queue.Enqueue(item); // After enqueuing an item, wake up any/all waiters; Monitor.PulseAll(m_lock); Monitor.Exit(m_lock); } public T Dequeue() { Monitor.Enter(m_lock); while (m_queue.Count == 0) Monitor.Wait(lock); T item = m_queue.Dequeue(); Monitor.Exit(m_lock); return item; } }
Using Collections to Aviod Holding a Lock for a Long Time
- System.Threading.Tasks所提供的往往更适合,为什么呢,比如Task:
- 比线程使用更少的内存,创建和销毁的时间更短;
- 线程池会根据CPU的情况自动扩容或收缩;
- Task完成一个阶段,执行Task的线程回到线程池,然后线程可以干点其他的事;
- 线程池是全局进程可见的,可以更好的调度任务,减少进程中的线程数,同时减少线程上下文切换的开销。
- Reader-Writer锁非常有用。Power Threading库有个非阻塞的读写类ReaderWriterGate。
The Concurrent Collection Classes
- FCL提供了4个线程安全的集合类型:System.Collections.Concurrent's (ConcurrentQueue, ConcurrentStack, ConcurrentDictionary)(MSCorLib.DLL), ConcurrentBag(System.DLL);
- ConcurrentDictionary内部使用Monitor;
- ConcurrentQueue, ConcurrentStack没有锁,内部使用Interlocked方法来维护集合;
- ConcurrentBag内部由每个线程一个迷你集合容器组成;
- ConcurrentStack, ConcurrentQueue, ConcurrentBag, .GetEnumerator方法返回集合内容的快照;
- ConcurrentDictionary.GetEnumerator方法不是返回内容的快照,要注意在枚举的过程中元素状态会发生改变。
本章小结
本章讲述的是混合的线程同步模式,首先通过一个简单的例子演示了如何混合使用用户模式和核心模式的同步结构。然后说明了轮转、线程所有制、锁递归的概念。接着列举了几种混合同步结构的实例,并进行了分析比较。本章还讨论了一个非常有意思的问题:单例模式的两次检查加锁情况,给出了正确实现单例模式的方法。然后讲了什么是条件变量模式,以及如何通过使用集合、Task和线程池来避免长时间持有锁。最后简单说明了四个并发集合类。