第三十章 混合线程同步构造
混合线程同步构造:合并了用户模式和内核模式构造。没有线程竞争时,混合构造提供了基元用户模式构造所具有的性能优势。多个线程竞争一个构造时,混合构造通过基元内核模式的构造来提供不“自旋”的优势。
30.1 一个简单的混合锁
internal sealed class SimpleHybirdLock : IDisposable
{ private Int32 m_waiters = 0; private AutoResetEvent m_waiterLock = new AutoResetEvent(false); public void Enter() { if(Interlockd.Increment(ref m_waiters )== 1) return; m_waiterLock.Wati } public void Leave() { if(Interlickd.Decrement(ref m_waiters) == 0 ) return; m_waiterLcok.Set(); } public void Dispose() { m_waiterLock.Dispose(); } }
30.2 自旋,线程所有权和递归
由于转换为内核模式会造成巨大的性能损失,而且线程占有锁的时间通常都很短,所以为了提升应用程序的总体性能,可以让一个线程在用户模式中“自旋”一小段时间,再让线程转换为内核模式。如果线程正在等待的锁在线程“自旋”期间变得可用,就能避免向内核模式的转换了。此外,有的锁限制只能由获得所得线程释放锁。有的锁允许当前拥有它的线程递归地拥有锁。
internal public sealed class AnotherHybridLock : IDisposable
{ private Int32 m_waiters = 0;
private AutoResetEvent m_waiterLock = new AutoResetEvent(false);
private Int32 m_spincout = 4000;
private Int32 m_owingThreadId = 0, m_recursion = 0;
public void Enter()
{
Int32 threadId = Thread.CurrentThread.ManagedThreadId;
if(threadId == m_owingThreadId){
m_recursion++; return;
}
SpinWait spinwait = new SpinWait();
} }
30.3 FCL中的混合构造
30.3.1 ManualResetEventSlim类和SemaphoreSilm类
两个构造的工作方式和对应的内核模式构造完全一致,只是它们都在用户模式中“自旋”,而且都推迟到发生第一次竞争时,才创建内核模式的构造。
30.3.2 Monitor类和同步块
堆中的每个对象都可关联一个名为同步块的数据结构。同步块包含字段。它为内核对象,拥有线程的ID,递归计数以及等待线程计数提供了相应的字段。
问题总结:
变量能引用一个代理对象-前提是变量引用的那个对象的类型派生自System.MarshalByRefObject类。
如果线程调用Monitor.Enter,向它传递对类型对象的引用,而且这个类型对象是以“AppDomain中立”的方式加载的,线程就会跨越进程中的所有AppDomain在那个类型上获取锁。
由于字符串可以留用,所以两个完全独立的代码段可能在不知情的情况下获取对内存的一个String对象引用。
跨越AppDomain边界传递字符串时,CLR不创建字符串的副本;相反,它只是将对字符串的一个引用传给其他AppDomain。
由于Monitor的方法要获取一个Object,所以传递类型会导致类型被装箱,造成现场在以装箱对象上获取锁。
向方法应用【MethodImpl(MethodImplOption.Synchronized)】特性,会造成JIT编译器用Monitor.Enter和Monitor.Exit调用包围方法的本机代码。
调用类型的类型构造器时,CLR要获取类型对象上的一个锁,确保只有一个线程初始化类型对象及其静态字段。
30.3.3 ReaderWriterLockSilm类
ReaderWriterLockSilm:
一个线程向数据写入时,请求访问的其他所有线程都被阻塞。
一个线程从数据读取时,请求的读取的其他线程允许继续执行,但请求写入的线程仍被阻塞。
向线程写入的线程结束后,要么解除一个写入线程的阻塞,使它能向数据写入,要么解除所有读取线程的阻塞,使它能并发读取数据。如果没有线程被阻塞,锁就进入可以自由使用的状态,可供下一个reader或writer线程获取。
从数据读取的所有线程结束后,一个writer线程被解除阻塞,使它能向数据写入,如果没有线程被阻塞,锁就进入可以自由使用的状态,可供下一个reader或writer线程获取。
30.3.4 OneManyLock类
30.3.5 CountdownEvent类
这个构造使用一个ManualResetEventSlim对象。这个构造阻塞一个线程,知道它的内部计数变成0。和Semaphore的行为相反。
30.3.6 Barrier类
30.3.7 线程同步构造小结
代码尽量不要阻塞任何线程。执行异步计算或I/O操作时,将数据从一个线程交给另一个线程时,应避免多个线程同时访问数据。如果不能完全做到这一点,请尽量使用Volatile和Interlocked的方法。
以下两种情况阻塞线程。
线程模型很简单。
线程有专门用途。
30.4 著名的双检锁技术
开发人员用“双检锁”将单实例对象的构造推迟到应用程序首次请求该对象时进行。也称“延迟初始化”。
30.5 条件变量模式
30.6 异步的同步构造
30.7 并发集合类