网页设计_Chrome多线程模型的优缺点_学聚网

Chrome多线程模型的优缺点

收藏到:

  

  

发布时间:2009-4-2 14:52:00

WebjxCom提示:一直在说Chrome在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快的境地.

开源是口好东西,它让这个充斥着大量工业垃圾代码和教材玩具代码的行业,多了一些艺术气息和美的潜质。它使得每个人,无论你来自米国纽约还是中国铁岭,都有机会站在巨人的肩膀上,如果不能,至少也可以抱一把大腿。。。

现在我就是来抱大腿的,这条粗腿隶属于Chrome(开源项目名称其实是Chromium,本来Chrome这个名字就够晦涩了,没想到它的本名还 更上一层楼...),Google那充满狼子野心的浏览器。每一个含着金勺子出生的人都免不了被仰慕并被唾骂,Chrome也不例外。关于Chrome的 优劣好坏讨论的太多了,基本已经被嚼成甘蔗渣了,没有人愿意再多张一口了。俗话说,内行看门道外行看热闹,大部分所谓的外行,是通过使用的真实感受来评定 优劣的,这无疑是最好的方式。但偏偏还是有自诩的内行,喜欢说内行话办外行事,一看到Chrome用到多进程就说垃圾废物肯定低能。拜托,大家都是搞技术 的,你知道多进程的缺点,Google也知道,他们不是政客,除了搞个噱头扯个蛋就一无所知了,人家也是有脸有皮的,写一坨屎一样的开源代码放出来遭世人 耻笑难道会很开心?所谓技术的优劣,是不能一概而论的,同样的技术在不同场合不同环境不同代码实现下,效果是有所不同的。既然Chrome用了很多看上去 不是很美的技术,我们是不是也需要了解一下它为什么要用,怎么用的,然后再开口说话?(恕不邀请,请自行对号入座...)。。。

人说是骡子是马拉出来遛遛,Google已经把Chrome这匹驴子拉到了世人面前,大家可以随意的遛。我们一直自诩是搞科学的,就是在努力和所谓 的艺术家拉开,人搞超女评委的,可以随意塞着屁眼用嘴放屁,楞把李天王说是李天后,你也只能说他是艺术品位独特。你要搞科学就不行,说的不对,轻的叫无 知,重的叫学术欺诈,结果一片惨淡。所以,既然代码都有了,再说话,就只能当点心注点意了,先看,再说。。。

我已经开始遛Chrome这头驴了,确切一点,是头壮硕的肥驴,项目总大小接近2G。这样的庞然大物要从头到脚每个毛孔的大量一遍,那估计不咽气也 要吐血的,咱又不是做Code review,不需要如此拼命。每一个好的开源项目,都像是一个美女,这世界没有十全十美的美女,自然也不会有样样杰出的开源项目。每个美女都有那么一两 点让你最心动不已或者倍感神秘的,你会把大部分的注意力都放在上面细细品味,看开源,也是一样。Chrome对我来说,有吸引力的地方在于(排名分先 后...):

它是如何利用多进程(其实也会有多线程一起)做并发的,又是如何解决多进程间的一些问题的,比如进程间通信,进程的开销; 做为一个后来者,它的扩展能力如何,如何去权衡对原有插件的兼容,提供怎么样的一个插件模型; 它的整体框架是怎样,有没有很NB的架构思想; 它如何实现跨平台的UI控件系统; 传说中的V8,为啥那么快。

但Chrome是一个跨平台的浏览器,其Linux和Mac版本正在开发过程中,所以我把所有的眼光都放在了windows版本中,所有的代码剖析都是基于windows版本的。话说,我本是浏览器新手、win api白痴以及并发处理的火星人,为了我的好奇投身到这个溜驴的行业中来,难免有学的不到位看的走眼的时候,各位看官手下超生,有错误请指正,实在看不下去,回家自己牵着遛吧。。。

扯淡实在是个体力活,所以后面我会少扯淡多说问题。。。

关于Chrome的源码下载和环境配置,大家看 这里(windows版本),只想强调一点,一定要严格按照说明来配置环境,特别是vs2005的补丁和windows SDK的安装,否则肯定是编译不过的。。。

最后,写这部分唯一不是废话的内容,请记住以下这幅图,这是Chrome最精华的一个缩影,如果你还有空,一定要去 这里 进行阅读,其中重中之重是 这一篇 。。。

图1 Chrome的线程和进程模型

WebjxCom提示:一直在说Chrome在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快的境地.

0. Chrome的并发模型

如果你仔细看了前面的图,对Chrome的线程和进程框架应该有了个基本的了解。Chrome有一个主进程,称为Browser进程,它是老大,管 理Chrome大部分的日常事务;其次,会有很多Renderer进程,它们圈地而治,各管理一组站点的显示和通信(Chrome在宣传中一直宣称一个 tab对应一个进程,其实是很不确切的...),它们彼此互不搭理,只和老大说话,由老大负责权衡各方利益。它们和老大说话的渠道,称做 IPC(Inter-Process Communication),这是Google搭的一套进程间通信的机制,基本的实现后面自会分解。。。

Chrome的进程模型

Google在宣传的时候一直都说,Chrome是one tab one process的模式,其实,这只是为了宣传起来方便如是说而已,基本等同广告,实际疗效,还要从代码中来看。实际上,Chrome支持的进程模型远比宣 传丰富,你可以参考一下这里 ,简单的说,Chrome支持以下几种进程模型:

Process-per-site-instance:就是你打开一个网站,然后从这个网站链开的一系列网站都属于一个进程。这是Chrome的默认模 式。 Process-per-site:同域名范畴的网站放在一个进程,比如www.google.com和 www.google.com/bookmarks就属于一个域名内(google有自己的判定机制),不论有没有互相打开的关系,都算作是一个进程中。 用命令行--process-per-site开启。 Process-per-tab:这个简单,一个tab一个process,不论各个tab的站点有无联系,就和宣传的那样。用--process- per-tab开启。 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最出彩的地方之一了。。。

闲话并发

单进程单线程的编程是最惬意的事情,所看即所得,一维的思考即可。但程序员的世界总是没有那么美好,在很多的场合,我们都需要有多线程、多进程、多机器携起手来一齐上阵共同完成某项任务,统称:并发(非官方版定义...)。在我看来,需要并发的场合主要是要两类:

为了更好的用户体验。有的事情处理起来太慢,比如数据库读写、远程通信、复杂计算等等,如果在一个线程一个进程里面来做,往往会影响用户感受,因此需要另 开一个线程或进程转到后台进行处理。它之所以能够生效,仰仗的是单CPU的分时机制,或者是多CPU协同工作。在单CPU的条件下,两个任务分成两拨完成 的总时间,是大于两个任务轮流完成的,但是由于彼此交错,更人的感觉更为的自然一些。 为了加速完成某项工作。大名鼎鼎的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 否 是 否 是否阻塞在信号量上 否 是 是

WebjxCom提示:一直在说Chrome在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快的境地.

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队列中去,等待执行。我知道很多人已经抄起了 板砖,因为这种手法实在是太常见了,就不是一个简单的依赖倒置,在线程池,Undo\Redo等模块的实现中,用的太多了。。。

但,我想说的是,虽说谁家过年都是吃顿饺子,这饺子好不好吃还是得看手艺,不能一概而论。在Chrome中,线程模型是统一且唯一的,这就相当于有了一套标准,它需要满足在各个线程上执行的几十上百种任务的需求,因此,必须在灵活行和易用性上有良好的表现,这就是设计标准的难度。为了满足这些需求,Chrome在底层库上做了足够的功夫:

它提供了一大套的模板封装(参见task.h),可以将Task摆脱继承结构、函数名、函数参数等限制(就是基于模板的伪function实现,想要更深入了解,建议直接看鼻祖《Modern C++》和它的Loki库...); 同时派生出CancelableTask、ReleaseTask、DeleteTask等子类,提供更为良好的默认实现; 在消息循环中,按逻辑的不同,将Task又分成即时处理的Task、延时处理的Task、Idle时处理的Task,满足不同场景的需求; 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总结了一下用锁的困难之处,我罚抄一遍,如下:

锁少加了,导致两个线程同时修改一个变量; 锁多加了,轻则妨碍并发,重则导致死锁; 锁加错了,由于锁和需要锁的数据之间的联系,只存在于程序员的大脑中,这种事情太容易发生了; 加锁的顺序错了,维护锁的顺序是一件困难而又容易出错的问题; 错误恢复; 忘记唤醒和错误的重试; 而最根本的缺陷,是锁和条件变量不支持模块化的编程。比如一个转账业务中,A账户扣了100元钱,B账户增加了100元,即使这两个动作单独用锁保护维持其正确性,你也不能将两个操作简单的串在一起完成一个转账操作,你必须让它们的锁都暴露出来,重新设计一番。好好的两个函数,愣是不能组在一起用,这就是锁的最大悲哀;

通过这些缺点的描述,也就可以明白Chrome多线程模型的优点。它解决了锁的最根本缺陷,即,支持模块化的编程,你只需要维护对象和线程之间的职能关系即可,这个摊子,比之锁的那个烂摊子,要简化了太多。对于程序员来说,负担一瞬间从泰山降成了鸿毛。。。

而Chrome多线程模型的一个主要难点,在于线程与数据关系的设计上,你需要良好的划分各个线程的职责,如果有一个线程所管辖的数据,几乎占据了大半部分的Task,那么它就会从多线程沦为单线程,Task队列的锁也将成为一个大大的瓶颈。。。

设计者的职责

一个底层结构设计是否成功,这个设计者是否称职,我一直觉得是有一个很简单的衡量标准的。你不需要看这个设计人用了多少NB的技术,你只需要关心, 他的设计,是否给其他开发人员带来了困难。一个NB的设计,是将所有困难都集中在底层搞定,把其他开发人员换成白痴都可以工作的那种;一个SB的设计,是 自己弄了半天,只是为了给其他开发人员一个长达250条的注意事项,然后很NB的说,你们按照这个手册去开发,就不会有问题了。。。

从根本上来说,Chrome的线程模型解决的是并发中的用户体验问题而不是联合工作的问题(参见我前面喷的“闲话并发”),它不是和 Map/Reduce那样将关注点放在数据和执行步骤的拆分上,而是放在线程和数据的对应关系上,这是和浏览器的工作环境相匹配的。设计总是和所处的环境 相互依赖的,毕竟,在客户端,不会和服务器一样,存在超规模的并发处理任务,而只是需要尽可能的改善用户体验,从这个角度来说,Chrome的多线程模型,至少看上去很美。。。

Chrome多线程模型的优缺点

收藏到:

  

  

发布时间:2009-4-2 14:52:00

WebjxCom提示:一直在说Chrome在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快的境地.

开源是口好东西,它让这个充斥着大量工业垃圾代码和教材玩具代码的行业,多了一些艺术气息和美的潜质。它使得每个人,无论你来自米国纽约还是中国铁岭,都有机会站在巨人的肩膀上,如果不能,至少也可以抱一把大腿。。。

现在我就是来抱大腿的,这条粗腿隶属于Chrome(开源项目名称其实是Chromium,本来Chrome这个名字就够晦涩了,没想到它的本名还 更上一层楼...),Google那充满狼子野心的浏览器。每一个含着金勺子出生的人都免不了被仰慕并被唾骂,Chrome也不例外。关于Chrome的 优劣好坏讨论的太多了,基本已经被嚼成甘蔗渣了,没有人愿意再多张一口了。俗话说,内行看门道外行看热闹,大部分所谓的外行,是通过使用的真实感受来评定 优劣的,这无疑是最好的方式。但偏偏还是有自诩的内行,喜欢说内行话办外行事,一看到Chrome用到多进程就说垃圾废物肯定低能。拜托,大家都是搞技术 的,你知道多进程的缺点,Google也知道,他们不是政客,除了搞个噱头扯个蛋就一无所知了,人家也是有脸有皮的,写一坨屎一样的开源代码放出来遭世人 耻笑难道会很开心?所谓技术的优劣,是不能一概而论的,同样的技术在不同场合不同环境不同代码实现下,效果是有所不同的。既然Chrome用了很多看上去 不是很美的技术,我们是不是也需要了解一下它为什么要用,怎么用的,然后再开口说话?(恕不邀请,请自行对号入座...)。。。

人说是骡子是马拉出来遛遛,Google已经把Chrome这匹驴子拉到了世人面前,大家可以随意的遛。我们一直自诩是搞科学的,就是在努力和所谓 的艺术家拉开,人搞超女评委的,可以随意塞着屁眼用嘴放屁,楞把李天王说是李天后,你也只能说他是艺术品位独特。你要搞科学就不行,说的不对,轻的叫无 知,重的叫学术欺诈,结果一片惨淡。所以,既然代码都有了,再说话,就只能当点心注点意了,先看,再说。。。

我已经开始遛Chrome这头驴了,确切一点,是头壮硕的肥驴,项目总大小接近2G。这样的庞然大物要从头到脚每个毛孔的大量一遍,那估计不咽气也 要吐血的,咱又不是做Code review,不需要如此拼命。每一个好的开源项目,都像是一个美女,这世界没有十全十美的美女,自然也不会有样样杰出的开源项目。每个美女都有那么一两 点让你最心动不已或者倍感神秘的,你会把大部分的注意力都放在上面细细品味,看开源,也是一样。Chrome对我来说,有吸引力的地方在于(排名分先 后...):

它是如何利用多进程(其实也会有多线程一起)做并发的,又是如何解决多进程间的一些问题的,比如进程间通信,进程的开销; 做为一个后来者,它的扩展能力如何,如何去权衡对原有插件的兼容,提供怎么样的一个插件模型; 它的整体框架是怎样,有没有很NB的架构思想; 它如何实现跨平台的UI控件系统; 传说中的V8,为啥那么快。

但Chrome是一个跨平台的浏览器,其Linux和Mac版本正在开发过程中,所以我把所有的眼光都放在了windows版本中,所有的代码剖析都是基于windows版本的。话说,我本是浏览器新手、win api白痴以及并发处理的火星人,为了我的好奇投身到这个溜驴的行业中来,难免有学的不到位看的走眼的时候,各位看官手下超生,有错误请指正,实在看不下去,回家自己牵着遛吧。。。

扯淡实在是个体力活,所以后面我会少扯淡多说问题。。。

关于Chrome的源码下载和环境配置,大家看 这里(windows版本),只想强调一点,一定要严格按照说明来配置环境,特别是vs2005的补丁和windows SDK的安装,否则肯定是编译不过的。。。

最后,写这部分唯一不是废话的内容,请记住以下这幅图,这是Chrome最精华的一个缩影,如果你还有空,一定要去 这里 进行阅读,其中重中之重是 这一篇 。。。

图1 Chrome的线程和进程模型

WebjxCom提示:一直在说Chrome在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快的境地.

0. Chrome的并发模型

如果你仔细看了前面的图,对Chrome的线程和进程框架应该有了个基本的了解。Chrome有一个主进程,称为Browser进程,它是老大,管 理Chrome大部分的日常事务;其次,会有很多Renderer进程,它们圈地而治,各管理一组站点的显示和通信(Chrome在宣传中一直宣称一个 tab对应一个进程,其实是很不确切的...),它们彼此互不搭理,只和老大说话,由老大负责权衡各方利益。它们和老大说话的渠道,称做 IPC(Inter-Process Communication),这是Google搭的一套进程间通信的机制,基本的实现后面自会分解。。。

Chrome的进程模型

Google在宣传的时候一直都说,Chrome是one tab one process的模式,其实,这只是为了宣传起来方便如是说而已,基本等同广告,实际疗效,还要从代码中来看。实际上,Chrome支持的进程模型远比宣 传丰富,你可以参考一下这里 ,简单的说,Chrome支持以下几种进程模型:

Process-per-site-instance:就是你打开一个网站,然后从这个网站链开的一系列网站都属于一个进程。这是Chrome的默认模 式。 Process-per-site:同域名范畴的网站放在一个进程,比如www.google.com和 www.google.com/bookmarks就属于一个域名内(google有自己的判定机制),不论有没有互相打开的关系,都算作是一个进程中。 用命令行--process-per-site开启。 Process-per-tab:这个简单,一个tab一个process,不论各个tab的站点有无联系,就和宣传的那样。用--process- per-tab开启。 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最出彩的地方之一了。。。

闲话并发

单进程单线程的编程是最惬意的事情,所看即所得,一维的思考即可。但程序员的世界总是没有那么美好,在很多的场合,我们都需要有多线程、多进程、多机器携起手来一齐上阵共同完成某项任务,统称:并发(非官方版定义...)。在我看来,需要并发的场合主要是要两类:

为了更好的用户体验。有的事情处理起来太慢,比如数据库读写、远程通信、复杂计算等等,如果在一个线程一个进程里面来做,往往会影响用户感受,因此需要另 开一个线程或进程转到后台进行处理。它之所以能够生效,仰仗的是单CPU的分时机制,或者是多CPU协同工作。在单CPU的条件下,两个任务分成两拨完成 的总时间,是大于两个任务轮流完成的,但是由于彼此交错,更人的感觉更为的自然一些。 为了加速完成某项工作。大名鼎鼎的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 否 是 否 是否阻塞在信号量上 否 是 是

WebjxCom提示:一直在说Chrome在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快的境地.

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队列中去,等待执行。我知道很多人已经抄起了 板砖,因为这种手法实在是太常见了,就不是一个简单的依赖倒置,在线程池,Undo\Redo等模块的实现中,用的太多了。。。

但,我想说的是,虽说谁家过年都是吃顿饺子,这饺子好不好吃还是得看手艺,不能一概而论。在Chrome中,线程模型是统一且唯一的,这就相当于有了一套标准,它需要满足在各个线程上执行的几十上百种任务的需求,因此,必须在灵活行和易用性上有良好的表现,这就是设计标准的难度。为了满足这些需求,Chrome在底层库上做了足够的功夫:

它提供了一大套的模板封装(参见task.h),可以将Task摆脱继承结构、函数名、函数参数等限制(就是基于模板的伪function实现,想要更深入了解,建议直接看鼻祖《Modern C++》和它的Loki库...); 同时派生出CancelableTask、ReleaseTask、DeleteTask等子类,提供更为良好的默认实现; 在消息循环中,按逻辑的不同,将Task又分成即时处理的Task、延时处理的Task、Idle时处理的Task,满足不同场景的需求; 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总结了一下用锁的困难之处,我罚抄一遍,如下:

锁少加了,导致两个线程同时修改一个变量; 锁多加了,轻则妨碍并发,重则导致死锁; 锁加错了,由于锁和需要锁的数据之间的联系,只存在于程序员的大脑中,这种事情太容易发生了; 加锁的顺序错了,维护锁的顺序是一件困难而又容易出错的问题; 错误恢复; 忘记唤醒和错误的重试; 而最根本的缺陷,是锁和条件变量不支持模块化的编程。比如一个转账业务中,A账户扣了100元钱,B账户增加了100元,即使这两个动作单独用锁保护维持其正确性,你也不能将两个操作简单的串在一起完成一个转账操作,你必须让它们的锁都暴露出来,重新设计一番。好好的两个函数,愣是不能组在一起用,这就是锁的最大悲哀;

通过这些缺点的描述,也就可以明白Chrome多线程模型的优点。它解决了锁的最根本缺陷,即,支持模块化的编程,你只需要维护对象和线程之间的职能关系即可,这个摊子,比之锁的那个烂摊子,要简化了太多。对于程序员来说,负担一瞬间从泰山降成了鸿毛。。。

而Chrome多线程模型的一个主要难点,在于线程与数据关系的设计上,你需要良好的划分各个线程的职责,如果有一个线程所管辖的数据,几乎占据了大半部分的Task,那么它就会从多线程沦为单线程,Task队列的锁也将成为一个大大的瓶颈。。。

设计者的职责

一个底层结构设计是否成功,这个设计者是否称职,我一直觉得是有一个很简单的衡量标准的。你不需要看这个设计人用了多少NB的技术,你只需要关心, 他的设计,是否给其他开发人员带来了困难。一个NB的设计,是将所有困难都集中在底层搞定,把其他开发人员换成白痴都可以工作的那种;一个SB的设计,是 自己弄了半天,只是为了给其他开发人员一个长达250条的注意事项,然后很NB的说,你们按照这个手册去开发,就不会有问题了。。。

从根本上来说,Chrome的线程模型解决的是并发中的用户体验问题而不是联合工作的问题(参见我前面喷的“闲话并发”),它不是和 Map/Reduce那样将关注点放在数据和执行步骤的拆分上,而是放在线程和数据的对应关系上,这是和浏览器的工作环境相匹配的。设计总是和所处的环境 相互依赖的,毕竟,在客户端,不会和服务器一样,存在超规模的并发处理任务,而只是需要尽可能的改善用户体验,从这个角度来说,Chrome的多线程模型,至少看上去很美。。。


相关内容

  • 就业面试题
  • 1. 简述OC 中内存管理机制.与retain 配对使用的方法是dealloc 还是release , 为什么?需要与alloc 配对使用的方法是dealloc 还是release ,为什么?readwrite ,readonly ,assign ,retain ,copy ,nonatomic . ...

  • 歌尔声学笔试
  • 1 pwm是什么? 2 usb按照传输速率分哪几种,作用是什么 3 volatile的作用是什么? 4 改错:char *p="hello":char *q="world";char *k=strcat(p,q); 最后一道编程题,20分. 脉冲宽度调制(PW ...

  • 计算机三级数据库考点串讲资料
  • 数据库 一.基础知识 1.计算机系统的组成和应用领域. 计算机系统由(硬件) 系统.(软件) 系统两部份组成. 计算机的硬件基本上由五大部分组成:运算器.控制器.存储器.输入设备.输出设备. 软件系统分为(系统) 软件.(应用) 软件两部分. 计算机的应用领域包括什么? 1.科学计算 2.数据通信与 ...

  • 如何理解和认识Node
  • 如何理解和认识Node.js 摘要:Node 是一个服务器端 JavaScript 解释器,它将改变服务 器应该如何工作的概念.它的目标是帮助程序员构建高度可伸缩的应用程序,编写能够处理数万条同时连接到一个(只有一个)物理机的连接代码. 简介 如果您听说过 Node,或者阅读过一些文章,宣称 Nod ...

  • 培训教室方案-2014-6-23
  • 培训教室解决方案 万合集团信息技术分公司 2014.04.20 一.系统概述 当今,重视现代培训技术对培训产生的影响,大力推进培训现代化是世界培训发展的主流.我国在运用现代培训技术手段整合培训的过程中,已取得了相当成效.采用先进的培训手段,提供全新的培训环境,来设计培训活动,已经成为市场的趋势所在. ...

  • 连接池优缺点
  • 数据库连接池的好处 对于一个简单的数据库应用,由于对于数据库的访问不是很频繁.这时可以简单地在需要访问数据库时,就新创建一个连接,用完后就关闭它,这样做也不会带来什么明显的性能上的开销.但是对于一个复杂的数据库应用,情况就完全不同了.频繁的建立.关闭连接,会极大的减低系统的性能,因为对于连接的使用成 ...

  • 一个类只有一个实例,并且提供了一个全局的访问点.
  • 要求:一个类只有一个实例,并且提供了一个全局的访问点.注意:客户端在调用一个类的时候,不会考虑这个类是否只能有一个实例等问题的,所以这是设计者的问题. 实现: 实现1 public sealed class Singleton { private int count = 0; //to make s ...

  • 2013阿里巴巴笔试题
  • 一.单项选择题 1.下列说法不正确的是:(B) A.SATA硬盘的速度速度大约为500Mbps/s B.读取18XDVD光盘数据的速度为1Gbps C.前兆以太网的数据读取速度为1Gpbs D.读取DDR3内存数据的速度为100Gbps 解析:有说B的,有说D的,B肯定是不对的吧. 关于B选项: 理 ...

  • android面试题目及其答案大全
  • 面试题目及其答案 1,++i和i++ 区别 2,makefile文件代码,写代码 3,隐藏与重写的区别 4,linux 基本命令使用 如:如何查询帮助文档(man),如何查处文件(fiind),常用命令 5,进程间通信有几种,效率如何 6. 什么是嵌入式实时操作系统, Android 操作系统属于实 ...