看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。
|