作者:施懿民
链接:https://zhuanlan.zhihu.com/p/20866017
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

同步: 托管代码

托管代码可以访问很多在System.Threading里定义的同步原语。包括操作系统原语的简单封装如:互斥(Mutex),事件(Event)和旗标(Semaphore)对象,也包括类似的栅栏(Barrier)和自旋锁(SpinLock)等抽象。但托管代码用的最多的同步机制是System.Threading.Monitor,其提供了针对 任意托管对象 的高性能同步锁机制,还提供了被其保护的状态发生变化时的通知机制的“条件变量”语义。

Monitor是通过一个“混合锁”来实现的,其有自旋锁和类似互斥(Mutex)这些基于操作系统内核锁的功能。这个思路源自于大部分锁都是短暂获取的,因此自旋等待锁被释放的所耗费的时间比调用内核API从而阻塞线程更少。当然将CPU的时钟周期浪费在自旋上也是很严重的,因此如果锁在一段时间内没有被释放的话,那么CLR则会退回到调用内核API的实现上。

因为任意一个对象都是潜在的锁/条件变量,每个对象都需要有一个地方用来保存锁信息。这个就是在“对象头(object headers)”和“同步块(sync blocks)”里完成的。

对象头是一个在每个托管对象前面机器字长大小的字段。它在很多地方会用到,例如保存对象的哈希值。其中一个目的就是保存对象的锁状态。如果对象头需要保存更多的信息,我们通过创建一个“同步块”的方式扩充对象。

同步块保存在同步块表(Sync Block Table)里,通过同步块索引来寻址。对象的同步块索引保存在对象头里。

关于对象头和同步块的细节在syncblk.h/.cpp里定义。

如果对象头里还有空间,Monitor将锁住对象的线程的托管线程ID(如果没有线程锁住对象则是0)保存在其中。在这种情形下,获取锁的过程其实就是自旋等待对象头的线程ID为0,然后原子操作设置其值为当前线程的托管线程ID。

如果自旋一些次数后还不能获取锁,或对象头已经用作其它目的,那么就会为这个对象创建同步块。它包含一些额外数据,包括用来阻塞当前线程的事件对象,这样运行我们停止自旋并等待锁被释放。

一个用来作为条件变量的对象(通过Monitor.Wait 和 Monitor.Pulse)总是会被扩充的,因为同步块里已经没有足够的空间来保存必要的状态。

同步: 原生情况

CLR的原生部分也必须要有线程意识,因为其可能在多个线程上调用托管代码。这样要求原生的同步机制,例如锁,事件等等。

ITaskHost API 允许一个CLR宿主修改托管线程的很多方面,包括线程的创建、销毁和同步。这种允许宿主修改原生同步机制要求虚拟机的代码不能直接使用原生的同步原语(即临界区,互斥锁,事件等),而是需要使用虚拟机在其上的封装)。

除了上述细节之外,GC悬停是一个特殊的“锁”,而且几乎影响CLR的方方面面。如果必须处理GC堆上的对象,虚拟机的原生代码可能要进入“合作”模式,这样“GC悬停锁”就变成原生虚拟机代码里最重要的同步机制,在托管世界里也一样。

原生虚拟机代码里主要用到的同步机制是GC模式和Crst。

GC 模式

如上所述,所有托管线程都在合作模式中运行,因为其可能操作GC堆。一般来讲,原生代码不会碰托管对象,因此运行在优先模式。但有些虚拟机里的原生代码需要访问GC堆,需要运行在合作模式。

原生代码通常不会直接操作GC模式,而是通过两个宏:GCX_COOP and GCX_PREEMP 来进入期望的模式,并创建“支持物”以便线程在退出范围的时候返回到之前的模式。

需要注意的是GCX_COOP从GC堆上获取一个锁。在线程处于合作模式时,不能执行GC。而且原生线程也不能像托管线程那样被“劫持”,因此线程在切换回优先模式时都是处于合作模式。

因此在原生代码里进入合作模式是不被鼓励的。如果必须要进入合作模式,那么时间越短越好。线程在此模式时不能被阻塞,而且实际上不能安全的获取锁。

类似的,GCX_PREEMP 释放 线程拥有的锁。在进入优先模式之前必须要万分小心来确保所有GC引用都被妥善保护。

代码规范 文档描述了安全进行GC模式切换的必要原则。

Crst

正如Monitor对象是托管代码里推荐的锁机制,Crst是虚拟机代码里的推荐机制。与Monitor类似,Crst是一个知道宿主和GC模式的混合锁。Crst通过“层级锁”机制来规避死锁,该实现可参考 BotR的层级锁章节.

虽然有一些必须这么做的异常情况,在合作模式下获取一个Crst锁通常是不合适的。

特殊线程

除了托管代码创建的托管线程,CLR自身还创建了一些“特殊”线程。

终结者(Finalizer)线程

每个进程都创建了这个线程用来运行托管代码。当GC决定一个可终结(finalizable)的对象不再被引用,其将该对象置于终结队列。当GC结束后,终结者线程会被唤醒并处理队列里的所有终结对象。对象一个一个出列,其终结(finalizer)函数被依次调用。

该线程还用来处理一些CLR内部的清理工作,并等待一些外部事件通知(如低内存情形下,GC会被告知尽量凶悍的回收垃圾)。详情请参见GCHeap::FinalizerThreadStart。

GC 线程

当运行在“并行”或“服务器”模式时,GC创建一个或多个后台线程来并行执行垃圾回收的不同阶段。这些线程完成由GC管理,而且永远不会执行托管代码。

调试器线程

CLR为每个托管进程维护了一个原生线程,其用来在附加到托管调试器时执行多个调试操作。

应用程序域卸载线程

这个线程负责卸载应用程序域。其通过一个单独的CLR内部线程,而不是在请求卸载应用程序域的线程里完成。因为 a) 为卸载过程提供受保证的堆栈空间,b) 在必要时允许请求卸载的线程从应用程序域里向上展开。

线程池线程

CLR线程池维护一个托管线程集合用来执行用户的“工作”。这些托管线程都绑定到线程池管理的原生线程。线程池还维护一小部分的原生线程来处理类似“线程注入”,定时器以及“已注册的等待”等等功能。

CLR线程概览(下)的更多相关文章

  1. CLR线程概览(一)

    托管 vs. 原生线程 托管代码在“托管线程”上执行,(托管线程)与操作系统提供的原生线程不同.原生线程是在物理机器上执行的原生代码序列:而托管线程则是在CLR虚拟机上执行的虚拟线程. 正如JIT解释 ...

  2. 浅谈线程池(上):线程池的作用及CLR线程池

    原文地址:http://blog.zhaojie.me/2009/07/thread-pool-1-the-goal-and-the-clr-thread-pool.html 线程池是一个重要的概念. ...

  3. 线程机制、CLR线程池以及应用程序域

    最近在总结多线程.CLR线程池以及TPL编程实践,重读一遍CLR via C#,比刚上班的时候收获还是很大的.还得要多读书,读好书,同时要多总结,多实践,把技术研究透,使用好. 话不多说,直接上博文吧 ...

  4. C# 多线程学习系列三之CLR线程池系列之ThreadPool

    一.CLR线程池 1.进程和CLR的关系一个进程可以只包含一个CLR,也可以包含多个CLR2.CLR和AppDomain的关系一个CLR可以包含多个AppDomain3.CLR和线程池的关系一个CLR ...

  5. java-多线程(下)&GUI

    ###25.01_多线程(单例设计模式)(掌握) * 单例设计模式:保证类在内存中只有一个对象. * 如何保证类在内存中只有一个对象呢?     * (1)控制类的创建,不让其他类来创建本类的对象.p ...

  6. MVC的HTTP请求处理过程(IIS应用程序池、CLR线程池)

    主要内容 本文讲解的是:服务器接受Http Request请求之后,是如何进入.Net CLR,从而进一步操作的. 我们大家都知道,IIS必须先接受请求,然后才能有机会进入CLR,但对请求(reque ...

  7. CLR内部异常(下)

    直接使用SEH 有些情况里直接使用SEH会更合适一些.特别是,如果需要在第一次遍历(first pass - SEH异常处理流程里的第一遍处理)时需要执行某些操作时,也就是在堆栈向上展开之前,SEH是 ...

  8. CLR线程池

    WaitCallback 表示要在 ThreadPool 线程上执行的回调方法. 创建委托,方法是将回调方法传递给 WaitCallback 构造函数. 您的方法必须具有此处所显示的签名. 如果想使用 ...

  9. 如何应用CLR线程池来管理多线程

        class Program     {         static void Main(string[] args)         {             int intWorkerT ...

随机推荐

  1. 如何做优化,UITabelView才能更加顺滑 (转载)

    这篇文章是前两周@叶孤城叶大在微信群里面的分享,一直到这两天才翻出来研究.很多实用的东西,不过由于水平有限,有些地方没能翻译好,还请大家指正. 我已经在iOS这个最好的移动平台上有几年的开发经验了.在 ...

  2. OO中,先有对象还是先有类?

    就是问,在面向对象思想里,先有对象还是先有类,乍一看和先有鸡蛋还是先有鸡是一类问题,其实不然!这个问题,在lz考研复试的时候被面试官问过,一模一样,如今又在一个笔试题里看到了类似的题目,眨一下,有人会 ...

  3. table-cell实现宽度自适应布局

    利用table-cell可以实现宽度自适应布局. table-cell有一些比较好用的属性,比如垂直居中,自适应高度宽度等,为元素设置table-cell布局之后,元素的margin失效,paddin ...

  4. cookie相关

    参考百度百科: Cookie,有时也用其复数形式Cookies,指某些网站为了辨别用户身份.进行session跟踪而储存在用户本地终端上的数据(通常经过加密).定义于RFC2109和2965都已废弃, ...

  5. 一步一步开发Game服务器(二)登陆2

    上一篇文章,讲解了简单的登陆情况.接下来我们继续讲解登陆模块. 在正常的游戏服务器情况下.在尚未登录前可以查看服务器大区情况,登陆后也可以查看服务器大区情况,然后选择大区服务器.进行登录操作. 这样的 ...

  6. 介绍一种基于gulp对seajs的模块做合并压缩的方式

    之前的项目一直采用grunt来构建,然后用requirejs做模块化,requirejs官方有提供grunt的插件来做压缩合并.现在的项目切到了gulp,模块化用起了seajs,自然而然地也想到了模块 ...

  7. C#操作Mongodb

    因为MongoDb 跨平台,可以免费使用,读写效率高,集群搭建简单,可以水平扩展等各种因素. 我决定研究一下Mongodb,在查看了相关文档后发现它对C#的支持不错,而且还有现成的C#的驱动, 新版的 ...

  8. 模仿36。杀毒~button

    <Style x:Key="360btn" TargetType="{x:Type Button}"> <Setter Property=&q ...

  9. perl use FileHandle;打开多个文件

    use FileHandle;my %fh; my @filehandlename=("A","B","C"); ##文件句柄的名字: fo ...

  10. 安卓模拟器genymotion连接eclipse成功但是不显示其中项目

    安卓模拟器困了我两三天了,原装模拟器比较慢,忍受不了,查到安卓模拟器的神器——genymotion 按照网上的步骤一步步都安装完毕,最后打开后发现,genymotion界面里面没有找到新建的工程, 这 ...