这篇研究两个问题:chromium对线程的封装和进程通信。主要参考chromium的官方技术文档:Treading和Inter-process Communication (IPC)。
chrome速度快的优点,主要得益于线程模型的设计,这也是chrome最值得研究的技术点。
一、线程模型
chromium中的线程分为很多种类型,每个线程中有一个MessageLoop处理线程消息,对应实现:
Thread - base/threading/thread.h
MessageLoop - base/message_loop.h
线程的类型包括:io_thread,file_thread,db_thread,safe_browsing_thread等,其中io_thread是一个事件分发线程,管理browser进程和子进程的通信。(上一篇多进程架构图中可以看到它。)
chromium线程设计有一个非常重要的目标:保证UI线程的快速响应,也就是要确保UI线程中没有阻塞的I/O或耗时操作。保证这一点需要一个高效的多线程模型,面临的问题是:
1、想尽一切办法减少多线程锁;
2、多线程通信方式。
chromium的实现手段:
1、线程内消息循环的设计,为线程异步调用打下基础;
2、Task封装,解决了加锁的问题,同时用command模式统一同步和异步调用。
也就是说,Task是对一个任务处理的封装,而线程的消息循环都认识Task,这样整个多线程框架其实就是Task的传递和执行的过程,并且锁只在传递Task时发生。多么简单有效的模型!
消息循环
来看张图(引用自Chrome源码剖析)
图示将线程的消息循环处理描述的非常清楚,基本思路和windows的消息循环非常相似。
当然,不同类型的线程并不会处理所有的事件,但所有的消息循环都会处理Task。
线程消息循环分为MessageLoop和MessagePump,其中MessageLoop专门处理Task,而MessagePump处理其他消息,
发现Task时交给MessageLoop。不同线程的消息循环的MessageLoop和MessagePump如图:
Task
Task是对行为的封装,或者说是将处理函数封装为一个对象。Task对象有一个Run函数,供目标线程的消息循环调用,执行Task的处理。还是看张图:
如果A线程想让B线程处理一个事务,A只需创建一个Task,将事务处理代码封装好,找到B线程的MessageLoop对象,调用PostTask方法将Task插入消息队列。PostTask将立即返回,A线程继续处理自己的消息循环。B线程消息循环处理到Task时,调用其Run方法完成事务处理。
chromium对Task的创建和传递做了封装,并定义了丰富的Task类型,满足多种场景需要。同时支持Task自身的日志和统计,便于调试。
参考文章:
二、进程通信(IPC)
chromium的进程通信在Windows下采用命名管道(在Linux & Mac上采用socketpair),涉及到的进程主要是Browser process、Renderer process和Plugin process。
(具体可以参考我的上一篇博文。)实际上最主要的通信发生在Browser和Renderer之间,基本上每一个Renderer process对应一个named pipe与Browser通信。
管道通信采用异步模式,保证不会发生阻塞。
根据上一篇博文提到的多进程模型图,可以知道Browser process中有一个I/O线程专门处理IPC,I/O线程和main thread之间通过ChannelProxy传递消息,I/O线程的主要目的是为了处理请求resource等可能阻塞的动作。Renderer process则用其main thread处理进程消息收发。
命名管道根据定义好的规则生成,需要通信的两个进程都认识这个管道,之后就可以按照管道的技术特点通信了。
chromium将这些逻辑用IPC::Channel封装,Channel处理三件事情:Send,Listen,处理Watcher,看张图:
发送进程调用Channel::Send将消息放到自己某线程的消息队列中,消息循环发现消息的信号量激活则序列化消息写到管道中。
操作系统负责将管道内容进行传递并通知接收进程,接收进程有一个I/O类型的线程,其消息循环处理Watcher(参考前面的讲解),
当发现Watcher的信号量被激活(收到消息),调用Watcher中对应的OnObjectSignaled方法,该方法通知本进程的Channel去管道中读取数据,并调用Listener解析处理。
chromium用消息循环支撑了进程通信机制,并保证了异步处理防止阻塞。
上面提到的ChannelProxy与Channel是什么关系呢?chromium为了保证线程安全,将Channel设计为由每个进程的I/O线程处理。
如果进程中的非I/O线程想发送进程消息怎么办?一个简单思路是将消息发送封装成Task交给I/O线程去处理,chromium将这个处理封装成了ChannelProxy,方便使用。
ChannelProxy甚至支持接收到消息后将其返回给原始发送消息的线程。基于以上,Browser的main thread想发送进程消息都必须通过ChannelProxy让I/O thread处理。
有时需要同步的进程通信,这部分封装在ChannelProxy的子类SyncChannel中。
chromium中的消息有两类:Routed消息和Control消息。简单来说,Routed消息有明确去向,会关联到一个视图上;
Control消息不指定到视图,通常被创建管道的class处理。
chromium为消息和Channel的使用封装了一套宏和模板,来简化进程通信的调用。具体可以参考:Inter-process Communication (IPC)。