Chrome学习笔记(一):线程模型,消息循环

看Chrome已经有一段时间了,但是一直都没有沉淀些内容下来,是该写写笔记什么的了,免得自己忘记了。看的都是Windows平台下的代码,所以记录也都是记录的。。。废话。。

那么首先,先从最基础的东西记录起吧:Chrome的线程模型和消息循环。

1. 多线程的麻烦

多线程编程一直是一件麻烦的事情,线程执行的不确定性,资源的并发访问,一直困扰着众多程序员们。为了解决多线程编程的麻烦,大家想出了很多经典的方案:如:对资源直接加锁,角色模型CSPFP等等。他们的思想基本分为两类:一类是对存在并发访问资源直接加锁,第二类是避免资源被并发访问。前者存在许多问题,如死锁,优先级反转等等,而相对来说,后者会好很多,角色模型,CSP和FP就都属于后者,Chrome也是使用后者的思想来实现多线程的。

2. Chrome的线程模型

为了实现多线程,Chrome思路是简单且尽可能的少用锁,所以它在实现中并没有使用如角色模型之类的复杂的结构,而只是引入了自己的消息循环作为多线程的基础。它足够简单,方便使用,而且很容易实现跨平台。

相比平时的消息循环(如:Windows的消息循环,Linux中的epoll模型),它唯一增加的功能就是可以运行自定义的任务:Task。

如果在一个线程里面需要访问另一个线程的数据,则把接下来要运行的函数和参数包装成一个Task,并将其传递给另外一个线程,由另外一个线程来执行这个Task,从而避免关键数据的并发访问,而又因为任务执行是有顺序的,这样就保证了代码执行的确定性。

chrome-messageloop-task-simple

其实,这就是一个典型的Command模式,而通过这个模式,简单的在线程之间传递Task,就实现了Chrome的多线程模型。

3. Task

3.1. Task

为了统一所有消息循环中的任务调用方式,所有的任务的基类都是这个Task类,他唯一的方法就是run(),MessageLoop只需要调用这个虚函数即可。

如果为了简化大家的开发,Chrome可谓下足了功夫,光是一个Task,就提供了各式各样的派生类供大家使用,并提供了良好的实现。

  • 派生出来的Task有:CancalableTask,ReleaseTask,QuitTask等等。
  • 根据调用条件的不同,将Task又分为即时处理的Task、延时处理的Task和Idle时处理的Task。
  • 为了简化开发,还引入了RunnableMethod,封装对象的方法,减少我们自己实现Task的时间。
  • 调用PostTask时,还需要传入一个TrackedObject,用于追踪Task的产生位置,为调试做准备。

3.2. RunnableMethod

RunnableMethod是一个很非常有用的类,这个方法通过模版将一个对象和他的方法和参数封装成一个Task,抛入另外一个线程去工作,其中为了保证对象的生命周期,对象的指针必须有引用计数,如果这个Task跨线程调用的话,这个引用计数必须是线程安全的。参数则通过Tuple来进行封装。在Task执行的时候通过另外一个模版将Tuple解开成参数即可。

4. 线程和消息循环

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

5. 消息循环之MessageLoop

5.1. 减少锁的请求

一般我们在实现任务队列时,会简单的将任务队列加锁,以避免多线程的访问,但是这样每取一次任务,都要访问一次锁。一旦任务变多,效率上必然成问题。

Chrome为了实现上尽可能少的使用锁,在接收任务时用了两个队列:输入队列和工作队列。

当向MessageLoop中内抛Task时,首先会将Task抛入MessageLoop的输入队列中,等到工作队列处理完成之后,再将当前的输入队列放入工作队列中,继续执行。

chrome-messageloop-task

这样,就只有当将Task放入输入队列时才需要加锁,而平时执行Task时是无锁的,这样就减少了对锁的占用时间。

5.2. 延时任务

为了实现延时任务,在MessageLoop中除了输入队列和工作队列,还有两个队列:延迟延迟任务队列和需在顶层执行的延迟任务队列。

在工作队列执行的时候,如果发现当前任务是延迟任务,则将任务放入此延迟队列,之后再处理,而如果发现当前消息循环处于嵌套状态,而任务本身不允许嵌套,则放入需在顶层执行的延迟任务队列。

一旦MessagePump产生了执行延迟任务的回调,则将从这两个队列中拿出任务出来执行。

6. 消息循环之MessagePump

MessagePump是用来从系统获取消息回调,触发MessageLoop执行Task的类,对于不同种类的MessageLoop都有一个相对应的MessagePump,这是为了将不同线程的任务执行触发方式封装起来,并且为MessageLoop提供跨平台的功能,chrome才将这个变化的部分封装成了MessagePump。所以在MessagePump的实现中,大家就可以找到很多不同类型的MessagePump:如MessagePumpWin,MessagePumpLibEvent,这些就是不同平台上或者不同线程上的封装。

Windows上的MessagePump有三种:MessagePumpDefault,MessagePumpForUI和MessagePumpForIO,他们分别对应着MessageLoop,MessageLoopForUI和MessageLoopForIO。

下面我们从底层循环的实现,如何实现延时Task等等方面来看一下这些不同的MessagePump的实现方式:

6.1. MessagePumpDefault

MessagePumpDefault是用于支持最基础的MessageLoop的消息泵,他中间其实是用一个for循环,在中间死循环,每次循环都回调MessageLoop要求其处理新来的Task。不过这样CPU还不满了?当然Chrome不会仅仅这么傻,在这个Pump中还申请了一个Event,在Task都执行完毕了之后,就会开始等待这个Event,直到下个Task到来时SetEvent,或者通过等待超时到某个延迟Task可以被执行。

6.2. MessagePumpForUI

MessagePumpForUI是用于支持MessageLoopForUI的消息泵,他和默认消息泵的区别是他中间会运行一个Windows的消息循环,用于分发窗口的消息,另外他还增加了一些和窗口相关的Observer等等。

各位应该也想到了一个问题:如果在某个任务中出现了模态对话框等等的Windows内部的消息循环,那么新来的消息应该如何处理呢?

其实在这个消息泵启动的时候,会创建一个消息窗口,一旦有新的任务到来,都会像这个窗口Post一个消息,这样利用这个窗口,即便在Windows内部消息循环存在的情况下,也可以正常触发执行Task的逻辑。

既然有了消息窗口,那么触发延时Task的就很简单了,只需要给这个窗口设置一个定时器就可以了。

6.3. MessagePumpForIO

MessagePumpForIO是用于支持MessageLoopForIO的消息泵,他和默认消息泵的区别在于,他底层实际上是一个用完成端口组成的消息循环,这样不管是本地文件的读写,或者是网络收发,都可以看作是一次IO事件。而一旦有任务或者有延时Task到来,这个消息泵就会向完成端口中发送一个自定义的IO事件,从而触发MessageLoop处理Task。

同系列文章:
原创文章,转载请标明出处:Soul Orbit
本文链接地址:Chrome学习笔记(一):线程模型,消息循环