各种Slim后缀的类,在System.Threading命名空间中,可以看到若干个以Slim后缀结尾的类:ManualResetEventSlim,SemaphoreSlim,ReaderWriterLockSlim。除了最后一个,其余两个都是在基元内核模式中有一样的构造,但是这三个类都是原有构造的简化版,尤其是前两个,使用方式跟原有的一样,但是尽量避免使用操作系统的内核对象,而达到了轻量级的效果。比如在SemaphoreSlim中使用了内核构造ManualResetEvent,但是这个构造是通过延时初始化,没达到非不得已时都不使用。至于ReaderWriterLockSlim则在后面再介绍。
Monitor与lock,lock关键字可谓是最广为人知的一种实现多线程同步的手段,那么下面则又从一段代码说起

这个方法相当简单且无实际意义,它只是为了看编译器把这段代码编译成什么样子,通过查看IL如下

留意到IL代码中出现了try…finally语句块、Monitor.Enter与Monotor.Exit方法。然后把代码更改一下再编译看看IL

IL代码

代码比较相似,但并非等价,实际上与lock语句块等价的代码如下

那么既然lock本质上是调用了Monitor,那Monitor是如何通过对一个对象加锁,然后实现线程同步。原来每个在托管堆里面的对象都有两个固定的成员,一个指向该对象类型的指针,另一个是指向一个线程同步块索引。这个索引指向一个同步块数组的元素,Monitor对线程加锁就是靠这个同步块。按照Jeffrey(CLR via C#的作者)的说法同步块中有三个字段,所有权的线程Id,等待线程的数量,递归的次数。然而我通过另一批文章了解到线程同步块的成员并非单纯这几个,有兴趣的同学可以去阅读《揭示同步块索引》的文章,有两篇。 当Monitor需要为某个对象obj加锁时,它会检查obj的同步块索引有否为数组的某个索引,如果是-1的,则从数组中找出一个空闲的同步块与之关联,同时同步块的所有权线程Id就记录下当前线程的Id;当再次有线程调用Monitor的时候就会检查同步块的所有权Id和当前线程Id是否对应上,能对应上的就让其通过,在递归次数上加1,如果对应不上的就把该线程扔到一个就绪队列(这个队列实际上也是存在同步块里面)中,并将其阻塞;这个同步块会在调用Exit的时候检查递归次数确保递归完了就清除所有权线程Id。通过等待线程数量得知是否有线程在等待,如果有则从等待队列中取出线程并释放,否则就解除与同步块的关联,让同步块等待被下个被加锁的对象使用。










