托管代码能够访问不少在System.Threading里定义的同步原语。包括操做系统原语的简单封装如:互斥(Mutex),事件(Event)和旗标(Semaphore)对象,也包括相似的栅栏(Barrier)和自旋锁(SpinLock)等抽象。但托管代码用的最多的同步机制是System.Threading.Monitor,其提供了针对 任意托管对象 的高性能同步锁机制,还提供了被其保护的状态发生变化时的通知机制的“条件变量”语义。git
Monitor是经过一个“混合锁”来实现的,其有自旋锁和相似互斥(Mutex)这些基于操做系统内核锁的功能。这个思路源自于大部分锁都是短暂获取的,所以自旋等待锁被释放的所耗费的时间比调用内核API从而阻塞线程更少。固然将CPU的时钟周期浪费在自旋上也是很严重的,所以若是锁在一段时间内没有被释放的话,那么CLR则会退回到调用内核API的实现上。github
由于任意一个对象都是潜在的锁/条件变量,每一个对象都须要有一个地方用来保存锁信息。这个就是在“对象头(object headers)”和“同步块(sync blocks)”里完成的。安全
对象头是一个在每一个托管对象前面机器字长大小的字段。它在不少地方会用到,例如保存对象的哈希值。其中一个目的就是保存对象的锁状态。若是对象头须要保存更多的信息,咱们经过建立一个“同步块”的方式扩充对象。服务器
同步块保存在同步块表(Sync Block Table)里,经过同步块索引来寻址。对象的同步块索引保存在对象头里。ide
关于对象头和同步块的细节在syncblk.h/.cpp里定义。函数
若是对象头里还有空间,Monitor将锁住对象的线程的托管线程ID(若是没有线程锁住对象则是0)保存在其中。在这种情形下,获取锁的过程其实就是自旋等待对象头的线程ID为0,而后原子操做设置其值为当前线程的托管线程ID。性能
若是自旋一些次数后还不能获取锁,或对象头已经用做其它目的,那么就会为这个对象建立同步块。它包含一些额外数据,包括用来阻塞当前线程的事件对象,这样运行咱们中止自旋并等待锁被释放。ui
一个用来做为条件变量的对象(经过Monitor.Wait 和 Monitor.Pulse)老是会被扩充的,由于同步块里已经没有足够的空间来保存必要的状态。操作系统
CLR的原生部分也必需要有线程意识,由于其可能在多个线程上调用托管代码。这样要求原生的同步机制,例如锁,事件等等。线程
ITaskHost API 容许一个CLR宿主修改托管线程的不少方面,包括线程的建立、销毁和同步。这种容许宿主修改原生同步机制要求虚拟机的代码不能直接使用原生的同步原语(即临界区,互斥锁,事件等),而是须要使用虚拟机在其上的封装)。
除了上述细节以外,GC悬停是一个特殊的“锁”,并且几乎影响CLR的方方面面。若是必须处理GC堆上的对象,虚拟机的原生代码可能要进入“合做”模式,这样“GC悬停锁”就变成原生虚拟机代码里最重要的同步机制,在托管世界里也同样。
原生虚拟机代码里主要用到的同步机制是GC模式和Crst。
如上所述,全部托管线程都在合做模式中运行,由于其可能操做GC堆。通常来说,原生代码不会碰托管对象,所以运行在优先模式。但有些虚拟机里的原生代码须要访问GC堆,须要运行在合做模式。
原生代码一般不会直接操做GC模式,而是经过两个宏:GCX_COOP and GCX_PREEMP 来进入指望的模式,并建立“支持物”以便线程在退出范围的时候返回到以前的模式。
须要注意的是GCX_COOP从GC堆上获取一个锁。在线程处于合做模式时,不能执行GC。并且原生线程也不能像托管线程那样被“劫持”,所以线程在切换回优先模式时都是处于合做模式。
所以在原生代码里进入合做模式是不被鼓励的。若是必需要进入合做模式,那么时间越短越好。线程在此模式时不能被阻塞,并且实际上不能安全的获取锁。
相似的,GCX_PREEMP 释放 线程拥有的锁。在进入优先模式以前必需要万分当心来确保全部GC引用都被妥善保护。
代码规范 文档描述了安全进行GC模式切换的必要原则。
正如Monitor对象是托管代码里推荐的锁机制,Crst是虚拟机代码里的推荐机制。与Monitor相似,Crst是一个知道宿主和GC模式的混合锁。Crst经过“层级锁”机制来规避死锁,该实现可参考 BotR的层级锁章节.
虽然有一些必须这么作的异常状况,在合做模式下获取一个Crst锁一般是不合适的。
除了托管代码建立的托管线程,CLR自身还建立了一些“特殊”线程。
每一个进程都建立了这个线程用来运行托管代码。当GC决定一个可终结(finalizable)的对象再也不被引用,其将该对象置于终结队列。当GC结束后,终结者线程会被唤醒并处理队列里的全部终结对象。对象一个一个出列,其终结(finalizer)函数被依次调用。
该线程还用来处理一些CLR内部的清理工做,并等待一些外部事件通知(如低内存情形下,GC会被告知尽可能凶悍的回收垃圾)。详情请参见GCHeap::FinalizerThreadStart。
当运行在“并行”或“服务器”模式时,GC建立一个或多个后台线程来并行执行垃圾回收的不一样阶段。这些线程完成由GC管理,并且永远不会执行托管代码。
CLR为每一个托管进程维护了一个原生线程,其用来在附加到托管调试器时执行多个调试操做。
这个线程负责卸载应用程序域。其经过一个单独的CLR内部线程,而不是在请求卸载应用程序域的线程里完成。由于 a) 为卸载过程提供受保证的堆栈空间,b) 在必要时容许请求卸载的线程从应用程序域里向上展开。
CLR线程池维护一个托管线程集合用来执行用户的“工做”。这些托管线程都绑定到线程池管理的原生线程。线程池还维护一小部分的原生线程来处理相似“线程注入”,定时器以及“已注册的等待”等等功能。