托管代码在“托管线程”上执行,(托管线程)与操做系统提供的原生线程不一样。原生线程是在物理机器上执行的原生代码序列;而托管线程则是在CLR虚拟机上执行的虚拟线程。git
正如JIT解释器将“虚拟的”中间(IL)指令映射到物理机器上的原声指令,CLR线程基础架构将“虚拟的”托管线程映射到操做系统的原生线程上。github
在任意时刻,一个托管线程可能会也可能不会被分配到一个原生线程执行。例如,一个已经被建立(经过“new System.Threading.Thread”)可是未启动(经过“System.Threading.Thread.Start”)的托管线程不会被指派到原生线程上执行。相似的,虽然CLR在实际上不会这样作,可是一个托管线程在执行时可被切换到多个原生线程上执行。安全
托管代码里公开的Thread接口就是用来隐藏其底层原生线程的细节的:数据结构
CLR提供并实现了托管线程的抽象。好比说,虽然其不暴露操做系统的线程本地存储(TLS)机制,可是其提供了托管“线程静态”变量。相似的,虽然其不提供原生线程的“线程ID”,可是其提供与操做系统无关的“托管线程ID”。不过为了便于诊断问题,底层原生线程的一些细节能够经过System.Diagnostics命名空间里的类型得到。架构
托管线程还提供了原生线程一般不用的功能。第一,托管线程在堆栈上使用GC引用,这样CLR必须在GC的时候能够枚举(甚至可能修改)这些GC引用。为了实现这个目的,CLR必须“暂停”每一个托管线程(即中止执行以即可以发现全部的GC引用)。第二,当AppDomain卸载时,CLR必须保证没有线程在执行这个AppDomain里的代码。这要求CLR能够强制线程从AppDomain脱离,CLR经过在线程里注入ThreadAbortException来实现这点。函数
每一个托管线程都跟一个Thread对象关联,其在threads.h里定义。这个对象跟踪CLR关于托管对象所须要了解的全部东西。包括如线程的当前GC模式和堆栈帧链这些必需品,也包括为了性能因素建立的不少元素(如一些快速arena-style分配器)。oop
全部的Thread对象都保存在ThreadStore中(也在threads.h中定义),其时一个全部已知线程的列表。要遍历全部的托管线程,须要先获取ThreadStoreLock,再使用ThreadStore::GetAllThreadList来枚举全部的线程对象。这个列表也包含没有被指派原生线程的托管线程(如未启动的线程,或原生线程已经存在了)。布局
原生线程能够经过一个原生线程本地存储(TLS)槽来获取绑定到该原生线程的托管线程。这容许原生线程上运行的代码能够经过GetThread()获取对应的Thread对象。性能
另外,许多托管线程有一个与原生Thread对象相区别的 托管 Thread对象(System.Threading.Thread)。托管Thread对象提供了方法以便托管代码与线程交互,其大部分是原生Thread对象功能的封装。经过Thread.CurrentThread能够(在托管代码中)获取到当前的托管线程对象。操作系统
在调试器里,“!Threads”这个SOS扩展命令能够用来枚举ThreadStore里的全部Thread对象。
一个托管线程在下列这些情形中建立:
在#1和#2这些情形中,CLR负责建立支撑托管线程的原生线程。这个只会在线程实际上启动了才会发生。在这些情形里,CLR“负责”原生线程;CLR负责原生线程的生命周期,因为CLR建立了它,所以也就知道线程的存在。
在#3和#4这些情形里,原生线程在托管线程以前就存在了,并且由CLR以外的代码负责。CLR不负责这种原生线程的生命周期。CLR只是在其第一次调用托管代码时意识到其存在。
当一个原生线程结束时,CLR经过其DllMain函数得到通知。这在操做系统的“加载锁”中发生,因此在处理这个通知的时候只能作不多(安全)的事情。与其销毁与托管线程关联的数据结构,这个线程只是被简单地标识成“死亡”状态,并启动finalizer线程。finalizer线程会遍历ThreadStore里全部死亡且托管代码再也不使用的线程。
CLR必须能够找到托管对象的全部引用以便执行GC。托管代码一直在不停的访问GC堆,操做堆栈和寄存器上的引用。CLR必须保证全部线程停在安全可靠的位置(这样他们不会修改GC堆),以便找到全部的托管对象。它只会停在安全点,这个时候能够在寄存器和堆栈上检查全部可用的引用。
另外一个办法就是GC堆、每一个线程的堆栈和寄存器状态都是所谓的“共享状态”,可被多个线程访问。正如大多数共享状态同样,须要一些“锁”来保护它们。托管代码在访问堆以前必需要获取锁,而且在安全的时候释放锁。
CLR将这种“锁”称做线程的“GC模式”。当线程获取锁的时候,处于“合做模式(cooperative mode)”;其必须与GC“合做”(经过释放锁)才能容许进行垃圾回收。而线程没有获取锁的时候,处于“优先模式(preemptive mode)” - GC能够“优先”进行垃圾回收,由于其知道线程没有访问GC堆。
GC只有在全部线程都处于“优先”模式(即没有获取锁)时才能进行垃圾回收。将全部线程移到优先模式的过程就称为“GC悬停(GC suspension)”或“暂停执行引擎”。
一个不大成熟的实现“锁”的方案是要求每一个托管线程在访问GC堆的时候实际获取和释放保护它的锁。而后GC会向每一个线程尝试获取锁,一旦其获取全部线程的锁,就能够安全的进行垃圾回收了。
然而,上面的方案由于两个缘由而显得不足。第一,这会要求托管代码耗费大量的时间在于获取和释放锁(或至少是检查GC是否在尝试获取锁 - 也就是“GC轮询 GC poll - 即不停的向GC轮询”)。第二,它要求JIT解释器生成大量的“GC信息代码”,以描述每一行JIT生成的代码后的堆栈的布局和寄存器状态,这些信息会耗费大量的内存。
咱们针对上述办法的改进方案是,将JIT后的托管代码区分红“部分可中断”和“所有可中断”的代码。在部分可中断代码中,调用其余函数的地方是惟一的安全点,且JIT生成显式的“GC轮询”点以便检查是否有等待的GC。(JIT)只须要在这些地方生成GC信息。在所有可中断代码里,每一个指令都是一个安全点,JIT为每一个指令生成GC信息 - 可是其不生成“GC”轮询代码。所有可中断代码而是经过劫持线程(该过程在后文讲解)来进入“中断”状态。JIT基于代码质量,GC信息的大小以及GC悬停的时间延迟这些因素来断定是产生所有或部分可中断代码。
基于上述信息,定义了三个基础操做:进入合做模式,离开合做模式以及暂停执行引擎。
一个线程经过调用Thread::DisablePreemptiveGC进入合做模式。其为当前线程获取“锁”:
两个步骤其实是原子操做。
一个线程经过调用Thread::EnablePreemptiveGC来进入优先模式(释放锁)。其经过标识线程再也不进入合做模式来完成,并通知GC线程能够启动执行。
当GC开始运行时,第一步就是中断执行引擎。GCHeap::SuspendEE函数就是用来干这个的:
为了GC悬停而进行的劫持操做是经过Thread::SysSuspendForGC函数完成的。这个函数经过强制全部运行在合做模式的托管线程在“安全点”离开合做模式。其经过枚举全部的托管线程(经过遍历ThreadStore),针对每一个运行在合做模式中的托管线程:
为了卸载一个应用程序域(AppDomain),CLR须要保证没有线程运行在这个应用程序域中。为了实现这点,全部托管线程都被枚举,而任何堆栈上有属于被卸载应用程序域的帧的线程都被“中断”。一个ThreadAbortException异常被注入正在运行的线程,并致使线程向上展开(一直运行拆除代码)直到没有运行在这个应用程序域当中的堆栈帧,而ThreadAbortException也被转换成一个AppDomainUnloaded异常。
ThreadAbortException是一个很特别的异常。其也许会被用户代码捕捉到,可是CLR确保其在用户的异常处理代码以后再次被抛出。所以ThreadAbortException有时被称做“没法被捕捉”的,尽管严格来讲不是这样的。
ThreadAbortException一般经过在托管线程上设置一个标志位标志其“正在终止”来抛出的。CLR不少地方都会检查这个标志位(特别要注意的,每次从p/invoke返回),而且常常有设置这个标志位的目的就是为了让线程及时终止的情形。
然而,好比说,线程正在运行一个长时间的托管循环,那么它可能根本不会检查这个标志位。为了让这样的线程快速终止,线程就被“劫持”并强制抛出ThreadAbortException异常。劫持过程跟GC悬停很相似,只是线程跳转过去的代码块抛出ThreadAbortException,而不是等待GC运行完毕。
这种劫持意味着ThreadAbortException可能在任意位置发生。这样使得托管代码很难正确处理ThreadAbortException异常。所以除了在卸载应用程序域的时候使用这种机制之外 - 保证由ThreadAbort损坏的状态都跟应用程序域一块儿被清理,在其余地方使用它都不是很明智的选择。