SlideShare uma empresa Scribd logo
1 de 24
chromium 源码剖析(一)
开源是口好东西,它让这个充斥着大量工业垃圾代码和教材玩具代码的行业,多了一些艺术气息和美的潜质。它使得每
个人,无论你来自米国纽约还是中国铁岭,都有机会站在巨人的肩膀上,如果不能,至少也可以抱一把大腿。。。
现在我就是来抱大腿的,这条粗腿隶属于 Chrome(开源项目名称其实是 Chromium,本来 Chrome 这个名字就够晦
涩了,没想到它的本名还更上一层楼...),Google 那充满狼子野心的浏览器。每一个含着金勺子出生的人都免不了被
仰慕并被唾骂,Chrome 也不例外。关于 Chrome 的优劣好坏讨论的太多了,基本已经被嚼成甘蔗渣了,没有人愿意
再多张一口了。俗话说,内行看门道外行看热闹,大部分所谓的外行,是通过使用的真实感受来评定优劣的,这无疑是
最好的方式。但偏偏还是有自诩的内行,喜欢说内行话办外行事,一看到 Chrome 用到多进程就说垃圾废物肯定低能。
拜托,大家都是搞技术的,你知道多进程的缺点,Google 也知道,他们不是政客,除了搞个噱头扯个蛋就一无所知了,
人家也是有脸有皮的,写一坨屎一样的开源代码放出来遭世人耻笑难道会很开心?所谓技术的优劣,是不能一概而论的,
同样的技术在不同场合不同环境不同代码实现下,效果是有所不同的。既然 Chrome 用了很多看上去不是很美的技术,
我们是不是也需要了解一下它为什么要用,怎么用的,然后再开口说话?(恕不邀请,请自行对号入座...)。。。
人说是骡子是马拉出来遛遛,Google 已经把 Chrome 这匹驴子拉到了世人面前,大家可以随意的遛。我们一直自诩是
搞科学的,就是在努力和所谓的艺术家拉开,人搞超女评委的,可以随意塞着屁眼用嘴放屁,楞把李天王说是李天后,
你也只能说他是艺术品位独特。你要搞科学就不行,说的不对,轻的叫无知,重的叫学术欺诈,结果一片惨淡。所以,
既然代码都有了,再说话,就只能当点心注点意了,先看,再说。。。
我已经开始遛 Chrome 这头驴了,确切一点,是头壮硕的肥驴,项目总大小接近 2G。这样的庞然大物要从头到脚每个
毛孔的大量一遍,那估计不咽气也要吐血的,咱又不是做 Code review,不需要如此拼命。每一个好的开源项目,都
像是一个美女,这世界没有十全十美的美女,自然也不会有样样杰出的开源项目。每个美女都有那么一两点让你最心动
不已或者倍感神秘的,你会把大部分的注意力都放在上面细细品味,看开源,也是一样。Chrome 对我来说,有吸引力
的地方在于(排名分先后...):
1. 它是如何利用多进程(其实也会有多线程一起)做并发的,又是如何解决多进程间的一些问题的,比如进程间通信,
进程的开销;
2. 做为一个后来者,它的扩展能力如何,如何去权衡对原有插件的兼容,提供怎么样的一个插件模型;
3. 它的整体框架是怎样,有没有很 NB 的架构思想;
4. 它如何实现跨平台的 UI 控件系统;
5. 传说中的 V8,为啥那么快。
但 Chrome 是一个跨平台的浏览器,其 Linux 和 Mac 版本正在开发过程中,所以我把所有的眼光都放在了 windows
版本中,所有的代码剖析都是基于 windows 版本的。话说,我本是浏览器新手、win api 白痴以及并发处理的火星人,
为了我的好奇投身到这个溜驴的行业中来,难免有学的不到位看的走眼的时候,各位看官手下超生,有错误请指正,实
在看不下去,回家自己牵着遛吧。。。
扯淡实在是个体力活,所以后面我会少扯淡多说问题。。。
关于 Chrome 的源码下载和环境配置,大家看这里(windows 版本),只想强调一点,一定要严格按照说明来配置环
境,特别是 vs2005 的补丁和 windows SDK 的安装,否则肯定是编译不过的。。。
最后,写这部分唯一不是废话的内容,请记住以下这幅图,这是 Chrome 最精华的一个缩影,如果你还有空,一定要
去这里进行阅读,其中重中之重是这一篇。。。
图 1 Chrome 的线程和进程模型
【一】 Chrome 的多线程模型
0. Chrome 的并发模型
如果你仔细看了前面的图,对 Chrome 的线程和进程框架应该有了个基本的了解。Chrome 有一个主进程,称为
Browser 进程,它是老大,管理 Chrome 大部分的日常事务;其次,会有很多 Renderer 进程,它们圈地而治,各管
理一组站点的显示和通信(Chrome 在宣传中一直宣称一个 tab 对应一个进程,其实是很不确切的...),它们彼此互不
搭理,只和老大说话,由老大负责权衡各方利益。它们和老大说话的渠道,称做 IPC(Inter-Process Communication),
这是 Google 搭的一套进程间通信的机制,基本的实现后面自会分解。。。
Chrome 的进程模型
Google 在宣传的时候一直都说,Chrome 是 one tab one process 的模式,其实,
这只是为了宣传起来方便如是说而已,基本等同广告,实际疗效,还要从代码中
来看。实际上,Chrome 支持的进程模型远比宣传丰富,你可以参考一下这里 ,
简单的说,Chrome 支持以下几种进程模型:
1. Process-per-site-instance:就是你打开一个网站,然后从这个网
站链开的一系列网站都属于一个进程。这是 Chrome 的默认模式。
2. Process-per-site:同域名范畴的网站放在一个进程,比如
www.google.com 和www.google.com/bookmarks 就属于一个域名内
(google 有自己的判定机制),不论有没有互相打开的关系,都算
作是一个进程中。用命令行--process-per-site 开启。
3. Process-per-tab:这个简单,一个 tab 一个 process,不论各个
tab 的站点有无联系,就和宣传的那样。用--process-per-tab 开启。
4. Single Process:这个很熟悉了吧,传统浏览器的模式,没有多进程
只有多线程,用--single-process 开启。
关于各种模式的优缺点,官方有官方的说法,大家自己也会有自己的评述。不论
如何,至少可以说明,Google 不是由于白痴而采取多进程的策略,而是实验出
来的效果。。。
大家可以用 Shift+Esc 观察各模式下进程状况,至少我是观察失败了(每种都
和默认的一样...),原因待跟踪。。。
不论是 Browser 进程还是 Renderer 进程,都不只是光杆司令,它们都有一系列的线程为自己打理各种业务。对于
Renderer 进程,它们通常有两个线程,一个是 Main thread,它负责与老大进行联系,有一些幕后黑手的意思;另一
个是 Render thread,它们负责页面的渲染和交互,一看就知道是这个帮派的门脸级人物。相比之下,Browser 进程
既然是老大,小弟自然要多一些,除了大脑般的 Main thread,和负责与各 Renderer 帮派通信的 IO thread,其实还
包括负责管文件的 file thread,负责管数据库的 db thread 等等(一个更详细的列表,参见这里),它们各尽其责,
齐心协力为老大打拼。它们和各 Renderer 进程的之间的关系不一样,同一个进程内的线程,往往需要很多的协同工作,
这一坨线程间的并发管理,是 Chrome 最出彩的地方之一了。。。
闲话并发
单进程单线程的编程是最惬意的事情,所看即所得,一维的思考即可。但程序员
的世界总是没有那么美好,在很多的场合,我们都需要有多线程、多进程、多机
器携起手来一齐上阵共同完成某项任务,统称:并发(非官方版定义...)。在我
看来,需要并发的场合主要是要两类:
1. 为了更好的用户体验。有的事情处理起来太慢,比如数据库读写、远
程通信、复杂计算等等,如果在一个线程一个进程里面来做,往往会
影响用户感受,因此需要另开一个线程或进程转到后台进行处理。它
之所以能够生效,仰仗的是单 CPU 的分时机制,或者是多 CPU 协
同工作。在单 CPU 的条件下,两个任务分成两拨完成的总时间,是
大于两个任务轮流完成的,但是由于彼此交错,更人的感觉更为的自
然一些。
2. 为了加速完成某项工作。大名鼎鼎的 Map/Reduce,做的就是这样的
事情,它将一个大的任务,拆分成若干个小的任务,分配个若干个进
程去完成,各自收工后,在汇集在一起,更快的得到最后的结果。为
了达到这个目的,只有在多 CPU 的情形下才有可能,在单 CPU 的
场合(单机单 CPU...),是无法实现的。
在第二种场合下,我们会自然而然的关注数据的分离,从而很好的利用上多 CPU
的能力;而在第一种场合,我们习惯了单 CPU 的模式,往往不注重数据与行为
的对应关系,导致在多 CPU 的场景下,性能不升反降。。。
1. Chrome 的线程模型
仔细回忆一下我们大部分时候是怎么来用线程的,在我足够贫瘠的多线程经历中,往往都是这样用的:起一个线程,传
入一个特定的入口函数,看一下这个函数是否是有副作用的(Side Effect),如果有,并且还会涉及到多线程的数据访
问,仔细排查,在可疑地点上锁伺候。。。
Chrome 的线程模型走的是另一个路子,即,极力规避锁的存在。换更精确的描述方式来说,Chrome 的线程模型,将
锁限制了极小的范围内(仅仅在将 Task 放入消息队列的时候才存在...),并且使得上层完全不需要关心锁的问题(当
然,前提是遵循它的编程模型,将函数用 Task 封装并发送到合适的线程去执行...),大大简化了开发的逻辑。。。
不过,从实现来说,Chrome 的线程模型并没有什么神秘的地方(美女嘛,都是穿衣服比不穿衣服更有盼头...),它用
到了消息循环的手段。每一个 Chrome 的线程,入口函数都差不多,都是启动一个消息循环(参见 MessagePump
类),等待并执行任务。而其中,唯一的差别在于,根据线程处理事务类别的不同,所起的消息循环有所不同。比如处
理进程间通信的线程(注意,在 Chrome 中,这类线程都叫做 IO 线程,估计是当初设计的时候谁的脑门子拍错了...)
启用的是 MessagePumpForIO 类,处理 UI 的线程用的是 MessagePumpForUI 类,一般的线程用到的是
MessagePumpDefault 类(只讨论 windows, windows, windows...)。不同的消息循环类,主要差异有两个,
一是消息循环中需要处理什么样的消息和任务,第二个是循环流程(比如是死循环还是阻塞在某信号量上...)。下图是
一个完整版的 Chrome 消息循环图,包含处理 Windows 的消息,处理各种 Task(Task 是什么,稍后揭晓,敬请期
待...),处理各个信号量观察者(Watcher),然后阻塞在某个信号量上等待唤醒。。。
图 2 Chrome 的消息循环
当然,不是每一个消息循环类都需要跑那么一大圈的,有些线程,它不会涉及到那么多的事情和逻辑,白白浪费体力和
时间,实在是不可饶恕的。因此,在实现中,不同的 MessagePump 类,实现是有所不同的,详见下表:
MessagePumpDefault MessagePumpForIO MessagePumpForUI
是否需要处 否 是 是
理系统消息
是否需要处
理 Task
是 是 是
是否需要处
理 Watcher
否 是 否
是否阻塞在
信号量上
否 是 是
2. Chrome 中的 Task
从上面的表不难看出,不论是哪一种消息循环,必须处理的,就是 Task(暂且遗忘掉系统消息的处理和 Watcher,以
后,我们会缅怀它们的...)。刨去其它东西的干扰,只留下 Task 的话,我们可以这样认为:Chrome 中的线程从实
现层面来看没有任何区别,它的区别只存在于职责层面,不同职责的线程,会处理不同的 Task。最后,在铺天盖地西
红柿来临之前,我说一下啥是 Task。。。
简单的看,Task 就是一个类,一个包含了 void Run()抽象方法的类(参见 Task 类...)。一个真实的任务,可以派生
Task 类,并实现其 Run 方法。每个 MessagePump 类中,会有一个 MessagePump::Delegate 的类的对象
(MessagePump::Delegate 的一个实现,请参见 MessageLoop 类...),在这个对象中,会维护若干个 Task 的队
列。当你期望,你的一个逻辑在某个线程内执行的时候,你可以派生一个 Task,把你的逻辑封装在 Run 方法中,然后
实例一个对象,调用期望线程中的 PostTask 方法,将该 Task 对象放入到其 Task 队列中去,等待执行。我知道很多
人已经抄起了板砖,因为这种手法实在是太常见了,就不是一个简单的依赖倒置,在线程池,UndoRedo 等模块的实
现中,用的太多了。。。
但,我想说的是,虽说谁家过年都是吃顿饺子,这饺子好不好吃还是得看手艺,不能一概而论。在 Chrome 中,线程
模型是统一且唯一的,这就相当于有了一套标准,它需要满足在各个线程上执行的几十上百种任务的需求,因此,必须
在灵活行和易用性上有良好的表现,这就是设计标准的难度。为了满足这些需求,Chrome 在底层库上做了足够的功夫:
1. 它提供了一大套的模板封装(参见 task.h),可以将 Task 摆脱继承结构、函数名、函数参数等限制(就是
基于模板的伪 function 实现,想要更深入了解,建议直接看鼻祖《Modern C++》和它的 Loki 库...);
2. 同时派生出 CancelableTask、ReleaseTask、DeleteTask 等子类,提供更为良好的默认实现;
3. 在消息循环中,按逻辑的不同,将 Task 又分成即时处理的 Task、延时处理的 Task、Idle 时处理的 Task,
满足不同场景的需求;
4. Task 派生自 tracked_objects::Tracked,Tracked 是为了实现多线程环境下的日志记录、统计等功能,
使得 Task 天生就有良好的可调试性和可统计性;
这一套七荤八素的都搭建完,这才算是一个完整的 Task 模型,由此可知,这饺子,做的还是很费功夫的。。。
3. Chrome 的多线程模型
工欲善其事,必先利其器。Chrome 之所以费了老鼻子劲去磨底层框架这把刀,就是为了面对多线程这坨怪兽的时候杀
的更顺畅一些。在 Chrome 的多线程模型下,加锁这个事情只发生在将 Task 放入某线程的任务队列中,其他对任何
数据的操作都不需要加锁。当然,天下没有免费的午餐,为了合理传递 Task,你需要了解每一个数据对象所管辖的线
程,不过这个事情,与纷繁的加锁相比,真是小儿科了不知道多少倍。。。
图 3 Task 的执行模型
如果你熟悉设计模式,你会发现这是一个 Command 模式,将创建于执行的环境相分离,在一个线程中创建行为,在
另一个线程中执行行为。Command 模式的优点在于,将实现操作与构造操作解耦,这就避免了锁的问题,使得多线程
与单线程编程模型统一起来,其次,Command 还有一个优点,就是有利于命令的组合和扩展,在 Chrome 中,它有
效统一了同步和异步处理的逻辑。。。
Command 模式
Command 模式,是一种看上去很酷的模式,传统的面向对象编程,我们封
装的往往都是数据,在 Command 模式下,我们希望封装的是行为。这件事
在函数式编程中很正常,封装一个函数作为参数,传来传去,稀疏平常的事
儿;但在面向对象的编程中,我们需要通过继承、模板、函数指针等手法,
才能将其实现。。。
应用 Command 模式,我们是期望这个行为能到一个不同于它出生的环境中
去执行,简而言之,这是一种想生不想养的行为。我们做 Undo/Redo 的时
候,会把在任一一个环境中创建的 Command,放到一个队列环境中去,供
统一的调度;在 Chrome 中,也是如此,我们在一个线程环境中创建了 Task,
却把它放到别的线程中去执行,这种寄居蟹似的生活方式,在很多场合都是
有用武之地的。。。
在一般的多线程模型中,我们需要分清楚啥是同步啥是异步,在同步模式下,一切看上去和单线程没啥区别,但同时也
丧失了多线程的优势(沦落成为多线程串行...)。而如果采用异步的模式,那写起来就麻烦多了,你需要注册回调,小
心管理对象的生命周期,程序写出来是嗷嗷恶心。在 Chrome 的多线程模型下,同步和异步的编程模型区别就不复存
在了,如果是这样一个场景:A 线程需要 B 线程做一些事情,然后回到 A 线程继续做一些事情;在 Chrome 下你可以
这样来做:生成一个 Task,放到 B 线程的队列中,在该 Task 的 Run 方法最后,会生成另一个 Task,这个 Task 会
放回到 A 的线程队列,由 A 来执行。如此一来,同步异步,天下一统,都是 Task 传来传去,想不会,都难了。。。
图 4 Chrome 的一种异步执行的解决方案
4. Chrome 多线程模型的优缺点
一直在说 Chrome 在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快
的境地。《代码之美》的第二十四章“美丽的并发”中,Haskell 设计人之一的 Simon Peyton Jones 总结了一下用锁的
困难之处,我罚抄一遍,如下:
1. 锁少加了,导致两个线程同时修改一个变量;
2. 锁多加了,轻则妨碍并发,重则导致死锁;
3. 锁加错了,由于锁和需要锁的数据之间的联系,只存在于程序员的大脑中,这种事情太容易发生了;
4. 加锁的顺序错了,维护锁的顺序是一件困难而又容易出错的问题;
5. 错误恢复;
6. 忘记唤醒和错误的重试;
7. 而最根本的缺陷,是锁和条件变量不支持模块化的编程。比如一个转账业务中,A 账户扣了 100 元钱,B 账
户增加了 100 元,即使这两个动作单独用锁保护维持其正确性,你也不能将两个操作简单的串在一起完成一
个转账操作,你必须让它们的锁都暴露出来,重新设计一番。好好的两个函数,愣是不能组在一起用,这就
是锁的最大悲哀;
通过这些缺点的描述,也就可以明白 Chrome 多线程模型的优点。它解决了锁的最根本缺陷,即,支持模块化的编程,
你只需要维护对象和线程之间的职能关系即可,这个摊子,比之锁的那个烂摊子,要简化了太多。对于程序员来说,负
担一瞬间从泰山降成了鸿毛。。。
而 Chrome 多线程模型的一个主要难点,在于线程与数据关系的设计上,你需要良好的划分各个线程的职责,如果有
一个线程所管辖的数据,几乎占据了大半部分的 Task,那么它就会从多线程沦为单线程,Task 队列的锁也将成为一个
大大的瓶颈。。。
设计者的职责
一个底层结构设计是否成功,这个设计者是否称职,我一直觉得是有一个很
简单的衡量标准的。你不需要看这个设计人用了多少 NB 的技术,你只需要
关心,他的设计,是否给其他开发人员带来了困难。一个 NB 的设计,是将
所有困难都集中在底层搞定,把其他开发人员换成白痴都可以工作的那种;
一个 SB 的设计,是自己弄了半天,只是为了给其他开发人员一个长达 250
条的注意事项,然后很 NB 的说,你们按照这个手册去开发,就不会有问题
了。。。
从根本上来说,Chrome 的线程模型解决的是并发中的用户体验问题而不是联合工作的问题(参见我前面喷的“闲话并
发”),它不是和 Map/Reduce 那样将关注点放在数据和执行步骤的拆分上,而是放在线程和数据的对应关系上,这是
和浏览器的工作环境相匹配的。设计总是和所处的环境相互依赖的,毕竟,在客户端,不会和服务器一样,存在超规模
的并发处理任务,而只是需要尽可能的改善用户体验,从这个角度来说,Chrome 的多线程模型,至少看上去很美。。。
看 Chrome 已经有一段时间了,但是一直都没有沉淀些内容下来,是该写写笔记什么的了,免得自己忘记了。看的都是 Windows
平台下的代码,所以记录也都是记录的。。。废话。。
那么首先,先从最基础的东西记录起吧:Chrome 的线程模型和消息循环。
【二】多线程的麻烦
多线程编程一直是一件麻烦的事情,线程执行的不确定性,资源的并发访问,一直困扰着众多程序员们。为了解决多线程
编程的麻烦,大家想出了很多经典的方案:如:对资源直接加锁,角色模型,CSP,FP 等等。他们的思想基本分为两类:一类
是对存在并发访问资源直接加锁,第二类是避免资源被并发访问。前者存在许多问题,如死锁,优先级反转等等,而相对来说,
后者会好很多,角色模型,CSP 和 FP 就都属于后者,Chrome 也是使用后者的思想来实现多线程的。
Chrome 的线程模型
为了实现多线程,Chrome 思路是简单且尽可能的少用锁,所以它在实现中并没有使用如角色模型之类的复杂的结构,而只是
引入了自己的消息循环作为多线程的基础。它足够简单,方便使用,而且很容易实现跨平台。
相比平时的消息循环(如:Windows 的消息循环,Linux 中的 epoll 模型),它唯一增加的功能就是可以运行自定义的任务:
Task。
如果在一个线程里面需要访问另一个线程的数据,则把接下来要运行的函数和参数包装成一个 Task,并将其传递给另外一个线
程,由另外一个线程来执行这个 Task,从而避免关键数据的并发访问,而又因为任务执行是有顺序的,这样就保证了代码执行
的确定性。
chrome-messageloop-task-simple:
其实,这就是一个典型的 Command 模式,而通过这个模式,简单的在线程之间传递 Task,就实现了 Chrome 的多线程模型。
Task
1. Task
为了统一所有消息循环中的任务调用方式,所有的任务的基类都是这个 Task 类,他唯一的方法就是 run(),MessageLoop 只
需要调用这个虚函数即可。
如果为了简化大家的开发,Chrome 可谓下足了功夫,光是一个 Task,就提供了各式各样的派生类供大家使用,并提供了良好
的实现。
派生出来的 Task 有:CancalableTask,ReleaseTask,QuitTask 等等。
根据调用条件的不同,将 Task 又分为即时处理的 Task、延时处理的 Task 和 Idle 时处理的 Task。
为了简化开发,还引入了 RunnableMethod,封装对象的方法,减少我们自己实现 Task 的时间。
调用 PostTask 时,还需要传入一个 TrackedObject,用于追踪 Task 的产生位置,为调试做准备。
2. RunnableMethod
RunnableMethod 是一个很非常有用的类,这个方法通过模版将一个对象和他的方法和参数封装成一个 Task,抛入另外一个
线程去工作,其中为了保证对象的生命周期,对象的指针必须有引用计数,如果这个 Task 跨线程调用的话,这个引用计数必须
是线程安全的。参数则通过 Tuple 来进行封装。在 Task 执行的时候通过另外一个模版将 Tuple 解开成参数即可。
线程和消息循环
Chrome 将其线程分为了三类:普通线程,UI 线程和 IO 线程。他们之间的区别是:
普通线程:只能执行 Task,没有其他的功能。
UI 线程:所有的窗口都需要跑在 UI 线程上,它除了能执行 Task 以外,还能执行和界面相关的消息循环。
IO 线程:和本地文件读写,或者网络收发相关的操作都运行在这个线程上,它除了能执行 Task 以外,还能执行和 IO 操
作相关的事件回调。
由于这三类线程中 Task 的执行部分基本是一样的,而其他的功能却完成不同,为了实现这不同的三类线程,Chrome 将消息
循环分成了两个部分:MessageLoop 和 MessagePump。
chrome-thread-and-messageloop:
MessagePump 被提取出来负责执行 Task 的时机和处理线程本身的消息
,如:UI 线程的 Windows 消息,IO 线程的 IO 事件。
MessageLoop 则仅仅是做 Task 的管理,它实现了 MessagePump 的 Delegate 的接口,这样 MessagePump 就可以告诉
MessageLoop 何时应该处理 Task 了。
另外实现上虽然 Chrome 为这三种线程实现了三套MessageLoop,但是它们之间的区别,也仅限于暴露出现的MessagePump
的接口不同而已。
chrome-messageloop-class-diagram:
消息循环之 MessageLoop
1. 减少锁的请求
一般我们在实现任务队列时,会简单的将任务队列加锁,以避免多线程的访问,但是这样每取一次任务,都要访问一次锁。
一旦任务变多,效率上必然成问题。
Chrome 为了实现上尽可能少的使用锁,在接收任务时用了两个队列:输入队列和工作队列。
当向 MessageLoop 中内抛 Task 时,首先会将 Task 抛入 MessageLoop 的输入队列中,等到工作队列处理完成之后,
再将当前的输入队列放入工作队列中,继续执行。
chrome-messageloop-task:
这样,就只有当将 Task 放入输入队列时才需要加锁,而平时执行 Task 时是无锁的,这样就减少了对锁的占用时间。
2. 延时任务
为了实现延时任务,在 MessageLoop 中除了输入队列和工作队列,还有两个队列:延迟延迟任务队列和需在顶层执行的延迟
任务队列。
在工作队列执行的时候,如果发现当前任务是延迟任务,则将任务放入此延迟队列,之后再处理,而如果发现当前消息循环处
于嵌套状态,而任务本身不允许嵌套,则放入需在顶层执行的延迟任务队列。
一旦 MessagePump 产生了执行延迟任务的回调,则将从这两个队列中拿出任务出来执行。
消息循环之 MessagePump
MessagePump 是用来从系统获取消息回调,触发 MessageLoop 执行 Task 的类,对于不同种类的 MessageLoop 都有一个
相对应的 MessagePump,这是为了将不同线程的任务执行触发方式封装起来,并且为 MessageLoop 提供跨平台的功能,
chrome 才将这个变化的部分封装成了 MessagePump。所以在 MessagePump 的实现中,大家就可以找到很多不同类型的
MessagePump:如 MessagePumpWin,MessagePumpLibEvent,这些就是不同平台上或者不同线程上的封装。
Windows 上的 MessagePump 有三种:MessagePumpDefault,MessagePumpForUI 和 MessagePumpForIO,他们分
别对应着 MessageLoop,MessageLoopForUI 和 MessageLoopForIO。
下面我们从底层循环的实现,如何实现延时 Task 等等方面来看一下这些不同的 MessagePump 的实现方式:
1. MessagePumpDefault
MessagePumpDefault 是用于支持最基础的 MessageLoop 的消息泵,他中间其实是用一个 for 循环,在中间死循环,每次
循环都回调 MessageLoop 要求其处理新来的 Task。不过这样 CPU 还不满了?当然 Chrome 不会仅仅这么傻,在这个 Pump
中还申请了一个 Event,在 Task 都执行完毕了之后,就会开始等待这个 Event,直到下个 Task 到来时 SetEvent,或者通过
等待超时到某个延迟 Task 可以被执行。
2. MessagePumpForUI
MessagePumpForUI 是用于支持 MessageLoopForUI 的消息泵,他和默认消息泵的区别是他中间会运行一个 Windows 的
消息循环,用于分发窗口的消息,另外他还增加了一些和窗口相关的 Observer 等等。
各位应该也想到了一个问题:如果在某个任务中出现了模态对话框等等的 Windows 内部的消息循环,那么新来的消息应该如
何处理呢?
其实在这个消息泵启动的时候,会创建一个消息窗口,一旦有新的任务到来,都会像这个窗口 Post 一个消息,这样利用这个窗
口,即便在 Windows 内部消息循环存在的情况下,也可以正常触发执行 Task 的逻辑。
既然有了消息窗口,那么触发延时 Task 的就很简单了,只需要给这个窗口设置一个定时器就可以了。
3. MessagePumpForIO
MessagePumpForIO 是用于支持 MessageLoopForIO 的消息泵,他和默认消息泵的区别在于,他底层实际上是一个用完成
端口组成的消息循环,这样不管是本地文件的读写,或者是网络收发,都可以看作是一次 IO 事件。而一旦有任务或者有延时
Task 到来,这个消息泵就会向完成端口中发送一个自定义的 IO 事件,从而触发 MessageLoop 处理 Task。
【三】闲话 chromium 线程模型
今天和朋友聊到关于 chromium 的线程模型的东西,在网上查了半天竟然没有哪篇能说清楚的,于是
review 了一下代码写点东西出来备忘一下,其实 chromium(以下称为 chrome)在对待多线程这个
事情上的做法还是很值得一看的,chrome 并不推荐过多的使用锁和 threadsafe 对象,在线程之间直
接共享对象不可避免的设计操作原子性的问题;chrome 的解决之道是通过 command 模式将对象隔
离在各个线程之间,每个线程拥有自己的消息队列和消息循环(message_loop), 数据的访问都是
通过消息传递实现的。
这部分的代码写得还是比较漂亮,但是网上的文章对于这部分的解读基本是一笔带过,其实深入到代
码中,有不少值得学习的地方,比如 chrome 对于一个功能类抽象程度的把握,work_queue 和
incoming_queue 的设计,智能指针的实现(scoped_refptr)等, 对提升架构能力,还有 c++的功力
很要帮助。
自顶向下的讲吧,从 Thread 类开始。在代码面前,没有 magic,即使是 google 写的也一样。 Thread
类顾名思义就是代表着一个线程对象。继承自 PlatformThread::Delegate,这个 delegate 没什么好说
的 , 规 定 子 类 实 现 一 个 ThreadMain , 这 个 函 数 在 线 程 被 创 建 后 调 用 :
src/base/threading/platform_thread_win.cc 代码:
DWORD __stdcall ThreadFunc(void* params) {
ThreadParams* thread_params = static_cast<ThreadParams*>(params);
PlatformThread::Delegate* delegate = thread_params->delegate;
if (!thread_params->joinable)
base::ThreadRestrictions::SetSingletonAllowed(false);
delete thread_params;
delegate->ThreadMain();
return NULL;
}
// CreateThreadInternal() matches PlatformThread::Create(), except that
// |out_thread_handle| may be NULL, in which case a non-joinable thread is
// created.
bool CreateThreadInternal(size_t stack_size,
PlatformThread::Delegate* delegate,
PlatformThreadHandle* out_thread_handle) {
PlatformThreadHandle thread_handle;
unsigned int flags = 0;
if (stack_size > 0 && base::win::GetVersion() >= base::win::VERSION_XP) {
flags = STACK_SIZE_PARAM_IS_A_RESERVATION;
} else {
stack_size = 0;
}
ThreadParams* params = new ThreadParams;
params->delegate = delegate;
params->joinable = out_thread_handle != NULL;
// Using CreateThread here vs _beginthreadex makes thread creation a bit
// faster and doesn't require the loader lock to be available. Our code will
// have to work running on CreateThread() threads anyway, since we run
code
// on the Windows thread pool, etc. For some background on the
difference:
// http://www.microsoft.com/msj/1099/win32/win321099.aspx
thread_handle = CreateThread(
NULL, stack_size, ThreadFunc, params, flags, NULL);
if (!thread_handle) {
delete params;
return false;
}
if (out_thread_handle)
*out_thread_handle = thread_handle;
else
CloseHandle(thread_handle);
return true;
}
这段代码是 PlatformThread 的 windows 实现,可以看见在 ThreadFunc 函数的传入参数中包含一个
Thread 类对象(实现了 PlatformThread::Delegate)的最后显示的调用了 Thread 对象的 ThreadMain
方法。 我们回到 src/base/threading/thread.cc 中,看看 ThreadMain 的实现,发现线程对象在此时
创建了一个 MessageLoop 对象,然后在 Run 函数中调用了这个 MessageLoop 对象的 Run 方法,这
里提前剧透一下,MessageLoop.Run()是线程消息循环的主体,跑着一个 for(;;),每次循环会检查当
前线程的 task 队列是否有新 task 要做,如果没有的话(此时 task 队列为空),会 wait 一个 event, 直
到 event 被激发(有新的 task 被加入队列)。 和 windows 的消息队列很像是吧,没错,基本就是一
个意思,甚至在 windows 平台的实现就是创建了个不可见窗口,复用 windows 的消息队列. Thread
类基本就是这些,没什么神奇的是吧,至于一些 WorkPool 之类的实现,清楚了 Thread 和
MessageLoop 的关系后,基本也能看了 在有了对消息队列的大概认识后,看代码会轻松许多。
接下来我们要进入苦笔的 MessageLoop MessageLoopProxy MessagePump 三兄弟(?不应该是兄
弟,有可能是父母和兄弟的关系吧 XD). 在写下去之前,我想稍微谴责一下命名这几个东西的家伙(具
体原因后面会提到),因为当年也是一开始就读这块的代码,第一次看这三个东西几乎完全不知道他
们是什么关系,到底做什么事情,他们几个接口都很可恶的差不多。接下来我们逐个往下看吧
首先,我们先看看 MessageLoop 类,文件位于 src/base/message_loop.cc, src/base/message_loop.h
这个东西一看就很霸气外露,头文件和 cc 加起来居然有坑爹的 1000 多行,但是请不要被吓着,其实
内 容 真 心 不 多 。 先 看 类 的 定 义 class BASE_EXPORT MessageLoop : public
base::MessagePump::Delegate { … }, 上来就和 MessagePump 扯上了点关系,这个 Delegate 是要
求实现一系列的 DoWork 方法。既然这个 delegate 东西是 messageloop 和 messagepump 的纽带,
我们就看看具体是个什么东西: 代码:
bool MessageLoop::DoWork() {
if (!nestable_tasks_allowed_) {
// Task can't be executed right now.
return false;
}
for (;;) {
ReloadWorkQueue();
if (work_queue_.empty())
break;
// Execute oldest task.
do {
PendingTask pending_task = work_queue_.front();
work_queue_.pop();
if (!pending_task.delayed_run_time.is_null()) {
AddToDelayedWorkQueue(pending_task);
// If we changed the topmost task, then it is time to reschedule.
if (delayed_work_queue_.top().task.Equals(pending_task.task))
pump_->ScheduleDelayedWork(pending_task.delayed_run_time);
} else {
if (DeferOrRunPendingTask(pending_task))
return true;
}
} while (!work_queue_.empty());
}
// Nothing happened.
return false;
}
要不怎么说 google 是全世界最牛 b 的互联网公司,这代码写的清清楚楚掷地有声,不需要看懂每个
函数的意义,一看过去就明白了大概的意思:首先尝试从 workqueue 中加载一下 task, 如果为空的话
直接推出循环,否则一个个 task 做,直到没有新的 task 为止(work_queue_.empty() == true)。
当时我看到这段心中大喊坑爹,不对啊,不是说好的 message_loop 嘛,这尼玛也不会永远的 loop
下去嘛,task做完怎么就break了,原来是我先入为主的以为此处实现的是整个消息队列的消息循环,
但是我错了,这个 message_loop 实现的是挨个的做 task, 和维护 task 的等待队列. 真正的 loop 在
message_pump 里! 然后我满怀期待的打开了 message_pump.h,发现 google 为了跨平台,在每
个平台用不同工具实现了各种各样的 messagepump, 在 message_pump.h 中定义的是一个接口,但
是我们找到了 Run()方法的定义!不用说,这个是真正的消息循环,打开 message_pump_default.cc
一看,果不其然华丽丽的标准消息队列死循环出现在我的眼前。 代码:
void MessagePumpDefault::Run(Delegate* delegate) {
DCHECK(keep_running_) << "Quit must have been called outside of Run!";
for (;;) {
mac::ScopedNSAutoreleasePool autorelease_pool;
bool did_work = delegate->DoWork();
if (!keep_running_)
break;
did_work |= delegate->DoDelayedWork(&delayed_work_time_);
if (!keep_running_)
break;
if (did_work)
continue;
did_work = delegate->DoIdleWork();
if (!keep_running_)
break;
if (did_work)
continue;
if (delayed_work_time_.is_null()) {
event_.Wait();
} else {
TimeDelta delay = delayed_work_time_ - TimeTicks::Now();
if (delay > TimeDelta()) {
event_.TimedWait(delay);
} else {
// It looks like delayed_work_time_ indicates a time in the past, so we
// need to call DoDelayedWork now.
delayed_work_time_ = TimeTicks();
}
}
// Since event_ is auto-reset, we don't need to do anything special here
// other than service each delegate method.
}
keep_running_ = true;
}
到现在,我们大概清楚了 MessageLoop 和 MessagePump 的关系: MessagePump 维护着一个消
息循环,然后每次循环从 MessageLoop 的 task 队列中运行各种 task,直到 task 队列变空,然后 wait,
直到 MessageLoop 被插入新的 task, 此时 MessageLoop 会激发 MessagePump 的 waiting event,
让消息循环继续运行。豁然开朗了 至于 MessageLoopProxy,不过是一个对原始的 MessageLoop
的 work_queue 做了线程安全的封装而已。
当然,从 Thread 开始到这里我省略了很多的东西,比如 MessageLoop 的 work_queue 和
incoming_queue 互相倒腾的牛逼设计,如何实现线程安全(插入 task 队列时),各种平台的事件驱
动的消息队列实现等等等等,随便哪个都能讲一卡车,这个乐趣还是留给自己去细细品读吧。

Mais conteúdo relacionado

Semelhante a Chromium (7)

trace code tool 以及人月神話
trace code tool 以及人月神話trace code tool 以及人月神話
trace code tool 以及人月神話
 
Ria的强力后盾:rest+海量存储
Ria的强力后盾:rest+海量存储 Ria的强力后盾:rest+海量存储
Ria的强力后盾:rest+海量存储
 
C++工程实践
C++工程实践C++工程实践
C++工程实践
 
ajax_onlinemad
ajax_onlinemadajax_onlinemad
ajax_onlinemad
 
Py ladies 0928
Py ladies 0928Py ladies 0928
Py ladies 0928
 
Py ladies 0928
Py ladies 0928Py ladies 0928
Py ladies 0928
 
學好 node.js 不可不知的事
學好 node.js 不可不知的事學好 node.js 不可不知的事
學好 node.js 不可不知的事
 

Chromium

  • 1. chromium 源码剖析(一) 开源是口好东西,它让这个充斥着大量工业垃圾代码和教材玩具代码的行业,多了一些艺术气息和美的潜质。它使得每 个人,无论你来自米国纽约还是中国铁岭,都有机会站在巨人的肩膀上,如果不能,至少也可以抱一把大腿。。。 现在我就是来抱大腿的,这条粗腿隶属于 Chrome(开源项目名称其实是 Chromium,本来 Chrome 这个名字就够晦 涩了,没想到它的本名还更上一层楼...),Google 那充满狼子野心的浏览器。每一个含着金勺子出生的人都免不了被 仰慕并被唾骂,Chrome 也不例外。关于 Chrome 的优劣好坏讨论的太多了,基本已经被嚼成甘蔗渣了,没有人愿意 再多张一口了。俗话说,内行看门道外行看热闹,大部分所谓的外行,是通过使用的真实感受来评定优劣的,这无疑是 最好的方式。但偏偏还是有自诩的内行,喜欢说内行话办外行事,一看到 Chrome 用到多进程就说垃圾废物肯定低能。 拜托,大家都是搞技术的,你知道多进程的缺点,Google 也知道,他们不是政客,除了搞个噱头扯个蛋就一无所知了, 人家也是有脸有皮的,写一坨屎一样的开源代码放出来遭世人耻笑难道会很开心?所谓技术的优劣,是不能一概而论的, 同样的技术在不同场合不同环境不同代码实现下,效果是有所不同的。既然 Chrome 用了很多看上去不是很美的技术, 我们是不是也需要了解一下它为什么要用,怎么用的,然后再开口说话?(恕不邀请,请自行对号入座...)。。。 人说是骡子是马拉出来遛遛,Google 已经把 Chrome 这匹驴子拉到了世人面前,大家可以随意的遛。我们一直自诩是 搞科学的,就是在努力和所谓的艺术家拉开,人搞超女评委的,可以随意塞着屁眼用嘴放屁,楞把李天王说是李天后, 你也只能说他是艺术品位独特。你要搞科学就不行,说的不对,轻的叫无知,重的叫学术欺诈,结果一片惨淡。所以, 既然代码都有了,再说话,就只能当点心注点意了,先看,再说。。。 我已经开始遛 Chrome 这头驴了,确切一点,是头壮硕的肥驴,项目总大小接近 2G。这样的庞然大物要从头到脚每个 毛孔的大量一遍,那估计不咽气也要吐血的,咱又不是做 Code review,不需要如此拼命。每一个好的开源项目,都 像是一个美女,这世界没有十全十美的美女,自然也不会有样样杰出的开源项目。每个美女都有那么一两点让你最心动 不已或者倍感神秘的,你会把大部分的注意力都放在上面细细品味,看开源,也是一样。Chrome 对我来说,有吸引力 的地方在于(排名分先后...): 1. 它是如何利用多进程(其实也会有多线程一起)做并发的,又是如何解决多进程间的一些问题的,比如进程间通信, 进程的开销; 2. 做为一个后来者,它的扩展能力如何,如何去权衡对原有插件的兼容,提供怎么样的一个插件模型; 3. 它的整体框架是怎样,有没有很 NB 的架构思想; 4. 它如何实现跨平台的 UI 控件系统; 5. 传说中的 V8,为啥那么快。 但 Chrome 是一个跨平台的浏览器,其 Linux 和 Mac 版本正在开发过程中,所以我把所有的眼光都放在了 windows 版本中,所有的代码剖析都是基于 windows 版本的。话说,我本是浏览器新手、win api 白痴以及并发处理的火星人, 为了我的好奇投身到这个溜驴的行业中来,难免有学的不到位看的走眼的时候,各位看官手下超生,有错误请指正,实 在看不下去,回家自己牵着遛吧。。。 扯淡实在是个体力活,所以后面我会少扯淡多说问题。。。 关于 Chrome 的源码下载和环境配置,大家看这里(windows 版本),只想强调一点,一定要严格按照说明来配置环 境,特别是 vs2005 的补丁和 windows SDK 的安装,否则肯定是编译不过的。。。 最后,写这部分唯一不是废话的内容,请记住以下这幅图,这是 Chrome 最精华的一个缩影,如果你还有空,一定要 去这里进行阅读,其中重中之重是这一篇。。。
  • 2. 图 1 Chrome 的线程和进程模型 【一】 Chrome 的多线程模型 0. Chrome 的并发模型 如果你仔细看了前面的图,对 Chrome 的线程和进程框架应该有了个基本的了解。Chrome 有一个主进程,称为 Browser 进程,它是老大,管理 Chrome 大部分的日常事务;其次,会有很多 Renderer 进程,它们圈地而治,各管
  • 3. 理一组站点的显示和通信(Chrome 在宣传中一直宣称一个 tab 对应一个进程,其实是很不确切的...),它们彼此互不 搭理,只和老大说话,由老大负责权衡各方利益。它们和老大说话的渠道,称做 IPC(Inter-Process Communication), 这是 Google 搭的一套进程间通信的机制,基本的实现后面自会分解。。。 Chrome 的进程模型 Google 在宣传的时候一直都说,Chrome 是 one tab one process 的模式,其实, 这只是为了宣传起来方便如是说而已,基本等同广告,实际疗效,还要从代码中 来看。实际上,Chrome 支持的进程模型远比宣传丰富,你可以参考一下这里 , 简单的说,Chrome 支持以下几种进程模型: 1. Process-per-site-instance:就是你打开一个网站,然后从这个网 站链开的一系列网站都属于一个进程。这是 Chrome 的默认模式。 2. Process-per-site:同域名范畴的网站放在一个进程,比如 www.google.com 和www.google.com/bookmarks 就属于一个域名内 (google 有自己的判定机制),不论有没有互相打开的关系,都算 作是一个进程中。用命令行--process-per-site 开启。 3. Process-per-tab:这个简单,一个 tab 一个 process,不论各个 tab 的站点有无联系,就和宣传的那样。用--process-per-tab 开启。 4. Single Process:这个很熟悉了吧,传统浏览器的模式,没有多进程 只有多线程,用--single-process 开启。 关于各种模式的优缺点,官方有官方的说法,大家自己也会有自己的评述。不论 如何,至少可以说明,Google 不是由于白痴而采取多进程的策略,而是实验出 来的效果。。。 大家可以用 Shift+Esc 观察各模式下进程状况,至少我是观察失败了(每种都 和默认的一样...),原因待跟踪。。。
  • 4. 不论是 Browser 进程还是 Renderer 进程,都不只是光杆司令,它们都有一系列的线程为自己打理各种业务。对于 Renderer 进程,它们通常有两个线程,一个是 Main thread,它负责与老大进行联系,有一些幕后黑手的意思;另一 个是 Render thread,它们负责页面的渲染和交互,一看就知道是这个帮派的门脸级人物。相比之下,Browser 进程 既然是老大,小弟自然要多一些,除了大脑般的 Main thread,和负责与各 Renderer 帮派通信的 IO thread,其实还 包括负责管文件的 file thread,负责管数据库的 db thread 等等(一个更详细的列表,参见这里),它们各尽其责, 齐心协力为老大打拼。它们和各 Renderer 进程的之间的关系不一样,同一个进程内的线程,往往需要很多的协同工作, 这一坨线程间的并发管理,是 Chrome 最出彩的地方之一了。。。 闲话并发 单进程单线程的编程是最惬意的事情,所看即所得,一维的思考即可。但程序员 的世界总是没有那么美好,在很多的场合,我们都需要有多线程、多进程、多机 器携起手来一齐上阵共同完成某项任务,统称:并发(非官方版定义...)。在我 看来,需要并发的场合主要是要两类: 1. 为了更好的用户体验。有的事情处理起来太慢,比如数据库读写、远 程通信、复杂计算等等,如果在一个线程一个进程里面来做,往往会 影响用户感受,因此需要另开一个线程或进程转到后台进行处理。它 之所以能够生效,仰仗的是单 CPU 的分时机制,或者是多 CPU 协 同工作。在单 CPU 的条件下,两个任务分成两拨完成的总时间,是 大于两个任务轮流完成的,但是由于彼此交错,更人的感觉更为的自 然一些。 2. 为了加速完成某项工作。大名鼎鼎的 Map/Reduce,做的就是这样的 事情,它将一个大的任务,拆分成若干个小的任务,分配个若干个进 程去完成,各自收工后,在汇集在一起,更快的得到最后的结果。为 了达到这个目的,只有在多 CPU 的情形下才有可能,在单 CPU 的 场合(单机单 CPU...),是无法实现的。 在第二种场合下,我们会自然而然的关注数据的分离,从而很好的利用上多 CPU
  • 5. 的能力;而在第一种场合,我们习惯了单 CPU 的模式,往往不注重数据与行为 的对应关系,导致在多 CPU 的场景下,性能不升反降。。。 1. Chrome 的线程模型 仔细回忆一下我们大部分时候是怎么来用线程的,在我足够贫瘠的多线程经历中,往往都是这样用的:起一个线程,传 入一个特定的入口函数,看一下这个函数是否是有副作用的(Side Effect),如果有,并且还会涉及到多线程的数据访 问,仔细排查,在可疑地点上锁伺候。。。 Chrome 的线程模型走的是另一个路子,即,极力规避锁的存在。换更精确的描述方式来说,Chrome 的线程模型,将 锁限制了极小的范围内(仅仅在将 Task 放入消息队列的时候才存在...),并且使得上层完全不需要关心锁的问题(当 然,前提是遵循它的编程模型,将函数用 Task 封装并发送到合适的线程去执行...),大大简化了开发的逻辑。。。 不过,从实现来说,Chrome 的线程模型并没有什么神秘的地方(美女嘛,都是穿衣服比不穿衣服更有盼头...),它用 到了消息循环的手段。每一个 Chrome 的线程,入口函数都差不多,都是启动一个消息循环(参见 MessagePump 类),等待并执行任务。而其中,唯一的差别在于,根据线程处理事务类别的不同,所起的消息循环有所不同。比如处 理进程间通信的线程(注意,在 Chrome 中,这类线程都叫做 IO 线程,估计是当初设计的时候谁的脑门子拍错了...) 启用的是 MessagePumpForIO 类,处理 UI 的线程用的是 MessagePumpForUI 类,一般的线程用到的是 MessagePumpDefault 类(只讨论 windows, windows, windows...)。不同的消息循环类,主要差异有两个, 一是消息循环中需要处理什么样的消息和任务,第二个是循环流程(比如是死循环还是阻塞在某信号量上...)。下图是 一个完整版的 Chrome 消息循环图,包含处理 Windows 的消息,处理各种 Task(Task 是什么,稍后揭晓,敬请期 待...),处理各个信号量观察者(Watcher),然后阻塞在某个信号量上等待唤醒。。。
  • 6. 图 2 Chrome 的消息循环 当然,不是每一个消息循环类都需要跑那么一大圈的,有些线程,它不会涉及到那么多的事情和逻辑,白白浪费体力和 时间,实在是不可饶恕的。因此,在实现中,不同的 MessagePump 类,实现是有所不同的,详见下表: MessagePumpDefault MessagePumpForIO MessagePumpForUI 是否需要处 否 是 是
  • 7. 理系统消息 是否需要处 理 Task 是 是 是 是否需要处 理 Watcher 否 是 否 是否阻塞在 信号量上 否 是 是 2. Chrome 中的 Task 从上面的表不难看出,不论是哪一种消息循环,必须处理的,就是 Task(暂且遗忘掉系统消息的处理和 Watcher,以 后,我们会缅怀它们的...)。刨去其它东西的干扰,只留下 Task 的话,我们可以这样认为:Chrome 中的线程从实 现层面来看没有任何区别,它的区别只存在于职责层面,不同职责的线程,会处理不同的 Task。最后,在铺天盖地西 红柿来临之前,我说一下啥是 Task。。。 简单的看,Task 就是一个类,一个包含了 void Run()抽象方法的类(参见 Task 类...)。一个真实的任务,可以派生 Task 类,并实现其 Run 方法。每个 MessagePump 类中,会有一个 MessagePump::Delegate 的类的对象 (MessagePump::Delegate 的一个实现,请参见 MessageLoop 类...),在这个对象中,会维护若干个 Task 的队 列。当你期望,你的一个逻辑在某个线程内执行的时候,你可以派生一个 Task,把你的逻辑封装在 Run 方法中,然后 实例一个对象,调用期望线程中的 PostTask 方法,将该 Task 对象放入到其 Task 队列中去,等待执行。我知道很多 人已经抄起了板砖,因为这种手法实在是太常见了,就不是一个简单的依赖倒置,在线程池,UndoRedo 等模块的实 现中,用的太多了。。。 但,我想说的是,虽说谁家过年都是吃顿饺子,这饺子好不好吃还是得看手艺,不能一概而论。在 Chrome 中,线程 模型是统一且唯一的,这就相当于有了一套标准,它需要满足在各个线程上执行的几十上百种任务的需求,因此,必须 在灵活行和易用性上有良好的表现,这就是设计标准的难度。为了满足这些需求,Chrome 在底层库上做了足够的功夫: 1. 它提供了一大套的模板封装(参见 task.h),可以将 Task 摆脱继承结构、函数名、函数参数等限制(就是 基于模板的伪 function 实现,想要更深入了解,建议直接看鼻祖《Modern C++》和它的 Loki 库...); 2. 同时派生出 CancelableTask、ReleaseTask、DeleteTask 等子类,提供更为良好的默认实现; 3. 在消息循环中,按逻辑的不同,将 Task 又分成即时处理的 Task、延时处理的 Task、Idle 时处理的 Task, 满足不同场景的需求; 4. Task 派生自 tracked_objects::Tracked,Tracked 是为了实现多线程环境下的日志记录、统计等功能, 使得 Task 天生就有良好的可调试性和可统计性; 这一套七荤八素的都搭建完,这才算是一个完整的 Task 模型,由此可知,这饺子,做的还是很费功夫的。。。
  • 8. 3. Chrome 的多线程模型 工欲善其事,必先利其器。Chrome 之所以费了老鼻子劲去磨底层框架这把刀,就是为了面对多线程这坨怪兽的时候杀 的更顺畅一些。在 Chrome 的多线程模型下,加锁这个事情只发生在将 Task 放入某线程的任务队列中,其他对任何 数据的操作都不需要加锁。当然,天下没有免费的午餐,为了合理传递 Task,你需要了解每一个数据对象所管辖的线 程,不过这个事情,与纷繁的加锁相比,真是小儿科了不知道多少倍。。。 图 3 Task 的执行模型
  • 9. 如果你熟悉设计模式,你会发现这是一个 Command 模式,将创建于执行的环境相分离,在一个线程中创建行为,在 另一个线程中执行行为。Command 模式的优点在于,将实现操作与构造操作解耦,这就避免了锁的问题,使得多线程 与单线程编程模型统一起来,其次,Command 还有一个优点,就是有利于命令的组合和扩展,在 Chrome 中,它有 效统一了同步和异步处理的逻辑。。。 Command 模式 Command 模式,是一种看上去很酷的模式,传统的面向对象编程,我们封 装的往往都是数据,在 Command 模式下,我们希望封装的是行为。这件事 在函数式编程中很正常,封装一个函数作为参数,传来传去,稀疏平常的事 儿;但在面向对象的编程中,我们需要通过继承、模板、函数指针等手法, 才能将其实现。。。 应用 Command 模式,我们是期望这个行为能到一个不同于它出生的环境中 去执行,简而言之,这是一种想生不想养的行为。我们做 Undo/Redo 的时 候,会把在任一一个环境中创建的 Command,放到一个队列环境中去,供 统一的调度;在 Chrome 中,也是如此,我们在一个线程环境中创建了 Task, 却把它放到别的线程中去执行,这种寄居蟹似的生活方式,在很多场合都是 有用武之地的。。。 在一般的多线程模型中,我们需要分清楚啥是同步啥是异步,在同步模式下,一切看上去和单线程没啥区别,但同时也 丧失了多线程的优势(沦落成为多线程串行...)。而如果采用异步的模式,那写起来就麻烦多了,你需要注册回调,小 心管理对象的生命周期,程序写出来是嗷嗷恶心。在 Chrome 的多线程模型下,同步和异步的编程模型区别就不复存 在了,如果是这样一个场景:A 线程需要 B 线程做一些事情,然后回到 A 线程继续做一些事情;在 Chrome 下你可以 这样来做:生成一个 Task,放到 B 线程的队列中,在该 Task 的 Run 方法最后,会生成另一个 Task,这个 Task 会 放回到 A 的线程队列,由 A 来执行。如此一来,同步异步,天下一统,都是 Task 传来传去,想不会,都难了。。。
  • 10. 图 4 Chrome 的一种异步执行的解决方案 4. Chrome 多线程模型的优缺点 一直在说 Chrome 在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快 的境地。《代码之美》的第二十四章“美丽的并发”中,Haskell 设计人之一的 Simon Peyton Jones 总结了一下用锁的 困难之处,我罚抄一遍,如下: 1. 锁少加了,导致两个线程同时修改一个变量; 2. 锁多加了,轻则妨碍并发,重则导致死锁; 3. 锁加错了,由于锁和需要锁的数据之间的联系,只存在于程序员的大脑中,这种事情太容易发生了; 4. 加锁的顺序错了,维护锁的顺序是一件困难而又容易出错的问题; 5. 错误恢复; 6. 忘记唤醒和错误的重试;
  • 11. 7. 而最根本的缺陷,是锁和条件变量不支持模块化的编程。比如一个转账业务中,A 账户扣了 100 元钱,B 账 户增加了 100 元,即使这两个动作单独用锁保护维持其正确性,你也不能将两个操作简单的串在一起完成一 个转账操作,你必须让它们的锁都暴露出来,重新设计一番。好好的两个函数,愣是不能组在一起用,这就 是锁的最大悲哀; 通过这些缺点的描述,也就可以明白 Chrome 多线程模型的优点。它解决了锁的最根本缺陷,即,支持模块化的编程, 你只需要维护对象和线程之间的职能关系即可,这个摊子,比之锁的那个烂摊子,要简化了太多。对于程序员来说,负 担一瞬间从泰山降成了鸿毛。。。 而 Chrome 多线程模型的一个主要难点,在于线程与数据关系的设计上,你需要良好的划分各个线程的职责,如果有 一个线程所管辖的数据,几乎占据了大半部分的 Task,那么它就会从多线程沦为单线程,Task 队列的锁也将成为一个 大大的瓶颈。。。 设计者的职责 一个底层结构设计是否成功,这个设计者是否称职,我一直觉得是有一个很 简单的衡量标准的。你不需要看这个设计人用了多少 NB 的技术,你只需要 关心,他的设计,是否给其他开发人员带来了困难。一个 NB 的设计,是将 所有困难都集中在底层搞定,把其他开发人员换成白痴都可以工作的那种; 一个 SB 的设计,是自己弄了半天,只是为了给其他开发人员一个长达 250 条的注意事项,然后很 NB 的说,你们按照这个手册去开发,就不会有问题 了。。。 从根本上来说,Chrome 的线程模型解决的是并发中的用户体验问题而不是联合工作的问题(参见我前面喷的“闲话并 发”),它不是和 Map/Reduce 那样将关注点放在数据和执行步骤的拆分上,而是放在线程和数据的对应关系上,这是 和浏览器的工作环境相匹配的。设计总是和所处的环境相互依赖的,毕竟,在客户端,不会和服务器一样,存在超规模 的并发处理任务,而只是需要尽可能的改善用户体验,从这个角度来说,Chrome 的多线程模型,至少看上去很美。。。 看 Chrome 已经有一段时间了,但是一直都没有沉淀些内容下来,是该写写笔记什么的了,免得自己忘记了。看的都是 Windows 平台下的代码,所以记录也都是记录的。。。废话。。 那么首先,先从最基础的东西记录起吧:Chrome 的线程模型和消息循环。 【二】多线程的麻烦 多线程编程一直是一件麻烦的事情,线程执行的不确定性,资源的并发访问,一直困扰着众多程序员们。为了解决多线程 编程的麻烦,大家想出了很多经典的方案:如:对资源直接加锁,角色模型,CSP,FP 等等。他们的思想基本分为两类:一类
  • 12. 是对存在并发访问资源直接加锁,第二类是避免资源被并发访问。前者存在许多问题,如死锁,优先级反转等等,而相对来说, 后者会好很多,角色模型,CSP 和 FP 就都属于后者,Chrome 也是使用后者的思想来实现多线程的。 Chrome 的线程模型 为了实现多线程,Chrome 思路是简单且尽可能的少用锁,所以它在实现中并没有使用如角色模型之类的复杂的结构,而只是 引入了自己的消息循环作为多线程的基础。它足够简单,方便使用,而且很容易实现跨平台。 相比平时的消息循环(如:Windows 的消息循环,Linux 中的 epoll 模型),它唯一增加的功能就是可以运行自定义的任务: Task。 如果在一个线程里面需要访问另一个线程的数据,则把接下来要运行的函数和参数包装成一个 Task,并将其传递给另外一个线 程,由另外一个线程来执行这个 Task,从而避免关键数据的并发访问,而又因为任务执行是有顺序的,这样就保证了代码执行 的确定性。 chrome-messageloop-task-simple: 其实,这就是一个典型的 Command 模式,而通过这个模式,简单的在线程之间传递 Task,就实现了 Chrome 的多线程模型。 Task 1. Task 为了统一所有消息循环中的任务调用方式,所有的任务的基类都是这个 Task 类,他唯一的方法就是 run(),MessageLoop 只 需要调用这个虚函数即可。 如果为了简化大家的开发,Chrome 可谓下足了功夫,光是一个 Task,就提供了各式各样的派生类供大家使用,并提供了良好 的实现。
  • 13. 派生出来的 Task 有:CancalableTask,ReleaseTask,QuitTask 等等。 根据调用条件的不同,将 Task 又分为即时处理的 Task、延时处理的 Task 和 Idle 时处理的 Task。 为了简化开发,还引入了 RunnableMethod,封装对象的方法,减少我们自己实现 Task 的时间。 调用 PostTask 时,还需要传入一个 TrackedObject,用于追踪 Task 的产生位置,为调试做准备。 2. RunnableMethod RunnableMethod 是一个很非常有用的类,这个方法通过模版将一个对象和他的方法和参数封装成一个 Task,抛入另外一个 线程去工作,其中为了保证对象的生命周期,对象的指针必须有引用计数,如果这个 Task 跨线程调用的话,这个引用计数必须 是线程安全的。参数则通过 Tuple 来进行封装。在 Task 执行的时候通过另外一个模版将 Tuple 解开成参数即可。 线程和消息循环 Chrome 将其线程分为了三类:普通线程,UI 线程和 IO 线程。他们之间的区别是: 普通线程:只能执行 Task,没有其他的功能。 UI 线程:所有的窗口都需要跑在 UI 线程上,它除了能执行 Task 以外,还能执行和界面相关的消息循环。 IO 线程:和本地文件读写,或者网络收发相关的操作都运行在这个线程上,它除了能执行 Task 以外,还能执行和 IO 操 作相关的事件回调。 由于这三类线程中 Task 的执行部分基本是一样的,而其他的功能却完成不同,为了实现这不同的三类线程,Chrome 将消息 循环分成了两个部分:MessageLoop 和 MessagePump。 chrome-thread-and-messageloop: MessagePump 被提取出来负责执行 Task 的时机和处理线程本身的消息 ,如:UI 线程的 Windows 消息,IO 线程的 IO 事件。 MessageLoop 则仅仅是做 Task 的管理,它实现了 MessagePump 的 Delegate 的接口,这样 MessagePump 就可以告诉 MessageLoop 何时应该处理 Task 了。 另外实现上虽然 Chrome 为这三种线程实现了三套MessageLoop,但是它们之间的区别,也仅限于暴露出现的MessagePump 的接口不同而已。
  • 14. chrome-messageloop-class-diagram: 消息循环之 MessageLoop 1. 减少锁的请求 一般我们在实现任务队列时,会简单的将任务队列加锁,以避免多线程的访问,但是这样每取一次任务,都要访问一次锁。 一旦任务变多,效率上必然成问题。 Chrome 为了实现上尽可能少的使用锁,在接收任务时用了两个队列:输入队列和工作队列。 当向 MessageLoop 中内抛 Task 时,首先会将 Task 抛入 MessageLoop 的输入队列中,等到工作队列处理完成之后, 再将当前的输入队列放入工作队列中,继续执行。 chrome-messageloop-task:
  • 15. 这样,就只有当将 Task 放入输入队列时才需要加锁,而平时执行 Task 时是无锁的,这样就减少了对锁的占用时间。 2. 延时任务 为了实现延时任务,在 MessageLoop 中除了输入队列和工作队列,还有两个队列:延迟延迟任务队列和需在顶层执行的延迟 任务队列。 在工作队列执行的时候,如果发现当前任务是延迟任务,则将任务放入此延迟队列,之后再处理,而如果发现当前消息循环处 于嵌套状态,而任务本身不允许嵌套,则放入需在顶层执行的延迟任务队列。 一旦 MessagePump 产生了执行延迟任务的回调,则将从这两个队列中拿出任务出来执行。 消息循环之 MessagePump MessagePump 是用来从系统获取消息回调,触发 MessageLoop 执行 Task 的类,对于不同种类的 MessageLoop 都有一个 相对应的 MessagePump,这是为了将不同线程的任务执行触发方式封装起来,并且为 MessageLoop 提供跨平台的功能, chrome 才将这个变化的部分封装成了 MessagePump。所以在 MessagePump 的实现中,大家就可以找到很多不同类型的 MessagePump:如 MessagePumpWin,MessagePumpLibEvent,这些就是不同平台上或者不同线程上的封装。 Windows 上的 MessagePump 有三种:MessagePumpDefault,MessagePumpForUI 和 MessagePumpForIO,他们分 别对应着 MessageLoop,MessageLoopForUI 和 MessageLoopForIO。 下面我们从底层循环的实现,如何实现延时 Task 等等方面来看一下这些不同的 MessagePump 的实现方式:
  • 16. 1. MessagePumpDefault MessagePumpDefault 是用于支持最基础的 MessageLoop 的消息泵,他中间其实是用一个 for 循环,在中间死循环,每次 循环都回调 MessageLoop 要求其处理新来的 Task。不过这样 CPU 还不满了?当然 Chrome 不会仅仅这么傻,在这个 Pump 中还申请了一个 Event,在 Task 都执行完毕了之后,就会开始等待这个 Event,直到下个 Task 到来时 SetEvent,或者通过 等待超时到某个延迟 Task 可以被执行。 2. MessagePumpForUI MessagePumpForUI 是用于支持 MessageLoopForUI 的消息泵,他和默认消息泵的区别是他中间会运行一个 Windows 的 消息循环,用于分发窗口的消息,另外他还增加了一些和窗口相关的 Observer 等等。 各位应该也想到了一个问题:如果在某个任务中出现了模态对话框等等的 Windows 内部的消息循环,那么新来的消息应该如 何处理呢? 其实在这个消息泵启动的时候,会创建一个消息窗口,一旦有新的任务到来,都会像这个窗口 Post 一个消息,这样利用这个窗 口,即便在 Windows 内部消息循环存在的情况下,也可以正常触发执行 Task 的逻辑。 既然有了消息窗口,那么触发延时 Task 的就很简单了,只需要给这个窗口设置一个定时器就可以了。 3. MessagePumpForIO MessagePumpForIO 是用于支持 MessageLoopForIO 的消息泵,他和默认消息泵的区别在于,他底层实际上是一个用完成 端口组成的消息循环,这样不管是本地文件的读写,或者是网络收发,都可以看作是一次 IO 事件。而一旦有任务或者有延时 Task 到来,这个消息泵就会向完成端口中发送一个自定义的 IO 事件,从而触发 MessageLoop 处理 Task。 【三】闲话 chromium 线程模型 今天和朋友聊到关于 chromium 的线程模型的东西,在网上查了半天竟然没有哪篇能说清楚的,于是 review 了一下代码写点东西出来备忘一下,其实 chromium(以下称为 chrome)在对待多线程这个 事情上的做法还是很值得一看的,chrome 并不推荐过多的使用锁和 threadsafe 对象,在线程之间直 接共享对象不可避免的设计操作原子性的问题;chrome 的解决之道是通过 command 模式将对象隔 离在各个线程之间,每个线程拥有自己的消息队列和消息循环(message_loop), 数据的访问都是 通过消息传递实现的。 这部分的代码写得还是比较漂亮,但是网上的文章对于这部分的解读基本是一笔带过,其实深入到代 码中,有不少值得学习的地方,比如 chrome 对于一个功能类抽象程度的把握,work_queue 和 incoming_queue 的设计,智能指针的实现(scoped_refptr)等, 对提升架构能力,还有 c++的功力 很要帮助。
  • 17. 自顶向下的讲吧,从 Thread 类开始。在代码面前,没有 magic,即使是 google 写的也一样。 Thread 类顾名思义就是代表着一个线程对象。继承自 PlatformThread::Delegate,这个 delegate 没什么好说 的 , 规 定 子 类 实 现 一 个 ThreadMain , 这 个 函 数 在 线 程 被 创 建 后 调 用 : src/base/threading/platform_thread_win.cc 代码: DWORD __stdcall ThreadFunc(void* params) { ThreadParams* thread_params = static_cast<ThreadParams*>(params); PlatformThread::Delegate* delegate = thread_params->delegate; if (!thread_params->joinable) base::ThreadRestrictions::SetSingletonAllowed(false); delete thread_params; delegate->ThreadMain(); return NULL; } // CreateThreadInternal() matches PlatformThread::Create(), except that // |out_thread_handle| may be NULL, in which case a non-joinable thread is // created. bool CreateThreadInternal(size_t stack_size, PlatformThread::Delegate* delegate, PlatformThreadHandle* out_thread_handle) { PlatformThreadHandle thread_handle; unsigned int flags = 0;
  • 18. if (stack_size > 0 && base::win::GetVersion() >= base::win::VERSION_XP) { flags = STACK_SIZE_PARAM_IS_A_RESERVATION; } else { stack_size = 0; } ThreadParams* params = new ThreadParams; params->delegate = delegate; params->joinable = out_thread_handle != NULL; // Using CreateThread here vs _beginthreadex makes thread creation a bit // faster and doesn't require the loader lock to be available. Our code will // have to work running on CreateThread() threads anyway, since we run code // on the Windows thread pool, etc. For some background on the difference: // http://www.microsoft.com/msj/1099/win32/win321099.aspx thread_handle = CreateThread( NULL, stack_size, ThreadFunc, params, flags, NULL); if (!thread_handle) { delete params; return false;
  • 19. } if (out_thread_handle) *out_thread_handle = thread_handle; else CloseHandle(thread_handle); return true; } 这段代码是 PlatformThread 的 windows 实现,可以看见在 ThreadFunc 函数的传入参数中包含一个 Thread 类对象(实现了 PlatformThread::Delegate)的最后显示的调用了 Thread 对象的 ThreadMain 方法。 我们回到 src/base/threading/thread.cc 中,看看 ThreadMain 的实现,发现线程对象在此时 创建了一个 MessageLoop 对象,然后在 Run 函数中调用了这个 MessageLoop 对象的 Run 方法,这 里提前剧透一下,MessageLoop.Run()是线程消息循环的主体,跑着一个 for(;;),每次循环会检查当 前线程的 task 队列是否有新 task 要做,如果没有的话(此时 task 队列为空),会 wait 一个 event, 直 到 event 被激发(有新的 task 被加入队列)。 和 windows 的消息队列很像是吧,没错,基本就是一 个意思,甚至在 windows 平台的实现就是创建了个不可见窗口,复用 windows 的消息队列. Thread 类基本就是这些,没什么神奇的是吧,至于一些 WorkPool 之类的实现,清楚了 Thread 和 MessageLoop 的关系后,基本也能看了 在有了对消息队列的大概认识后,看代码会轻松许多。 接下来我们要进入苦笔的 MessageLoop MessageLoopProxy MessagePump 三兄弟(?不应该是兄 弟,有可能是父母和兄弟的关系吧 XD). 在写下去之前,我想稍微谴责一下命名这几个东西的家伙(具
  • 20. 体原因后面会提到),因为当年也是一开始就读这块的代码,第一次看这三个东西几乎完全不知道他 们是什么关系,到底做什么事情,他们几个接口都很可恶的差不多。接下来我们逐个往下看吧 首先,我们先看看 MessageLoop 类,文件位于 src/base/message_loop.cc, src/base/message_loop.h 这个东西一看就很霸气外露,头文件和 cc 加起来居然有坑爹的 1000 多行,但是请不要被吓着,其实 内 容 真 心 不 多 。 先 看 类 的 定 义 class BASE_EXPORT MessageLoop : public base::MessagePump::Delegate { … }, 上来就和 MessagePump 扯上了点关系,这个 Delegate 是要 求实现一系列的 DoWork 方法。既然这个 delegate 东西是 messageloop 和 messagepump 的纽带, 我们就看看具体是个什么东西: 代码: bool MessageLoop::DoWork() { if (!nestable_tasks_allowed_) { // Task can't be executed right now. return false; } for (;;) { ReloadWorkQueue(); if (work_queue_.empty()) break; // Execute oldest task. do {
  • 21. PendingTask pending_task = work_queue_.front(); work_queue_.pop(); if (!pending_task.delayed_run_time.is_null()) { AddToDelayedWorkQueue(pending_task); // If we changed the topmost task, then it is time to reschedule. if (delayed_work_queue_.top().task.Equals(pending_task.task)) pump_->ScheduleDelayedWork(pending_task.delayed_run_time); } else { if (DeferOrRunPendingTask(pending_task)) return true; } } while (!work_queue_.empty()); } // Nothing happened. return false; } 要不怎么说 google 是全世界最牛 b 的互联网公司,这代码写的清清楚楚掷地有声,不需要看懂每个 函数的意义,一看过去就明白了大概的意思:首先尝试从 workqueue 中加载一下 task, 如果为空的话 直接推出循环,否则一个个 task 做,直到没有新的 task 为止(work_queue_.empty() == true)。
  • 22. 当时我看到这段心中大喊坑爹,不对啊,不是说好的 message_loop 嘛,这尼玛也不会永远的 loop 下去嘛,task做完怎么就break了,原来是我先入为主的以为此处实现的是整个消息队列的消息循环, 但是我错了,这个 message_loop 实现的是挨个的做 task, 和维护 task 的等待队列. 真正的 loop 在 message_pump 里! 然后我满怀期待的打开了 message_pump.h,发现 google 为了跨平台,在每 个平台用不同工具实现了各种各样的 messagepump, 在 message_pump.h 中定义的是一个接口,但 是我们找到了 Run()方法的定义!不用说,这个是真正的消息循环,打开 message_pump_default.cc 一看,果不其然华丽丽的标准消息队列死循环出现在我的眼前。 代码: void MessagePumpDefault::Run(Delegate* delegate) { DCHECK(keep_running_) << "Quit must have been called outside of Run!"; for (;;) { mac::ScopedNSAutoreleasePool autorelease_pool; bool did_work = delegate->DoWork(); if (!keep_running_) break; did_work |= delegate->DoDelayedWork(&delayed_work_time_); if (!keep_running_) break; if (did_work)
  • 23. continue; did_work = delegate->DoIdleWork(); if (!keep_running_) break; if (did_work) continue; if (delayed_work_time_.is_null()) { event_.Wait(); } else { TimeDelta delay = delayed_work_time_ - TimeTicks::Now(); if (delay > TimeDelta()) { event_.TimedWait(delay); } else { // It looks like delayed_work_time_ indicates a time in the past, so we // need to call DoDelayedWork now. delayed_work_time_ = TimeTicks(); } } // Since event_ is auto-reset, we don't need to do anything special here
  • 24. // other than service each delegate method. } keep_running_ = true; } 到现在,我们大概清楚了 MessageLoop 和 MessagePump 的关系: MessagePump 维护着一个消 息循环,然后每次循环从 MessageLoop 的 task 队列中运行各种 task,直到 task 队列变空,然后 wait, 直到 MessageLoop 被插入新的 task, 此时 MessageLoop 会激发 MessagePump 的 waiting event, 让消息循环继续运行。豁然开朗了 至于 MessageLoopProxy,不过是一个对原始的 MessageLoop 的 work_queue 做了线程安全的封装而已。 当然,从 Thread 开始到这里我省略了很多的东西,比如 MessageLoop 的 work_queue 和 incoming_queue 互相倒腾的牛逼设计,如何实现线程安全(插入 task 队列时),各种平台的事件驱 动的消息队列实现等等等等,随便哪个都能讲一卡车,这个乐趣还是留给自己去细细品读吧。