- 为什么使用多线程?
- Chrome的多线程模型主要解决什么问题?
- 如何实现该问题的解决?
1. 解决问题
Chrome有很多线程,这是为了保持UI线程(主线程)的高响应度,防止被其他费时的操作阻碍从而影响用户体验。但是多线程会造成资源并发访问引起的死锁和竞争冲突等问题。
2.方法
Chrome的多线程模型为避免资源被并发访问,尽量减少锁的使用,通过消息循环和自定义任务机制解决了并发问题。对于任一个线程,都是启动一个消息循环,等待和执行消息队列中的消息或任务。Chrome将需要的操作封装入自定义的任务Task中,由任务派发机制将Task传递给相应线程去执行,也只有在Task加入任务队列时需要加锁。线程之间数据传递也是通过封装Task进行通信。
3. 消息循环
Chrome中的主要线程根据其负责事务分为三类:
- 普通线程:只能执行Task,没有其他的功能。
- UI线程(也叫chrome线程):所有的窗口都需要跑在UI线程上,它除了能执行Task以外,还能执行和界面相关(UI)的消息循环。
- IO线程:和本地文件读写,或者网络收发相关的操作都运行在这个线程上,它除了能执行Task以外,还能执行和IO操作相关的事件回调。
结构:这三种线程处理的消息也可以相应分为三类:只处理Task,处理Task和UI消息,处理Task和IO消息。分别定义三个类来实现对消息的处理。其中MessageLoop作为基类处理Task,由它派生出来的两个类MessageLoopforUI和MessageLoopforIO分别处理另外两种消息类型以及相关平台信息(UI消息和IO操作是平台相关的)。为了结构清晰,定义一个新的基类及其子类来负责处理消息,这就是MessagePump。MessagePump的每个子类针对不同平台和不同的消息类型。事实上,不仅如此,消息处理的主循环也在MessagePump中。因此,MessageLoop通过实现MessagePumpDelegate的接口来负责处理自定义任务。
过程:消息处理的主循环在MessagePump中。消息循环开始,通过RunLoop接口调用MessageLoop,MessageLoop通过MessagePumpDelegate(消息代表)调用MessagePump,从MessagePump开始处理消息,对工作队列中的任务,根据其优先级分别执行(Work,DelayWork,IdleWork),处理完后判断是否还有待处理的任务,有则继续新一轮循环,无则暂停等待唤醒。(消息等待:对于IO消息等有OS提供支持,对于自定义的Task,则通过建立管道,在Task到来时写入一个字节从而唤醒消息循环?)
*待修改
【未整理】延时任务(延迟延迟任务队列和需在顶层执行的延迟任务队列),输入队列和工作队列(任务复制) |
4. Task
为了统一所有消息循环中的任务调用方式,所有的任务的基类都是这个Task类,他唯一的方法就是run(),MessageLoop只需要调用这个虚函数即可。如果为了简化开发,光是一个Task,就提供了各式各样的派生类。
- 它提供了一大套的模板封装(参见task.h),可以将Task摆脱继承结构、函数名、函数参数等限制。
- 派生出来的Task有:CancalableTask,ReleaseTask,QuitTask等等。
- 在消息循环中,根据不同的应用场景,将Task又分为即时处理的Task、延时处理的Task和Idle时处理的Task。
- 为了简化开发,还引入了RunnableMethod,封装对象的方法,减少我们自己实现Task的时间。
- 调用PostTask时,还需要传入一个tracked_objects::Tracked,Tracked是为了实现多线程环境下的日志记录、统计等功能,用于追踪Task的产生位置,为调试做准备,使得Task天生就有良好的可调试性和可统计性。
【未整理】Task在线程内创建,执行,抛出,加入,复制,销毁,…,的实现;