前面我们讲到了每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理DOM,又要计算样式,还要处理布局,同时还需要处理JavaScript任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是我们今天要讲的消息队列和事件循环系统。
单进程多任务的执行流程
在线程运行过程中处理新任务
上面的流程只能顺序执行没办法说插入个任务什么的。
要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制,取出一个task执行一个task
int GetInput(){
int input_number = 0;
cout<<"请输入一个数:";
cin>>input_number;
return input_number;
}
//主线程(Main Thread)
void MainThread(){
for(;;){
int first_num = GetInput();
int second_num = GetInput();
result_num = first_num + second_num;
print("最终计算的值为:%d",result_num);
}
}
相较于第一版的线程
第一点引入了循环机制,具体实现方式是在线程语句最后添加了一个for循环语句for循环语句,线程会一直循环行。
第二点是引入了事件,可以在线程运行过程中,等待用戶输入的数字,等待过程中线程处于暂停状态,一旦接收到用戶输入的信息,那么线程会被激活,然后执行相加运算,最后输出结果。
通过引入了事件循环让程序可以让线程活起来了,没人输入就等待 有输入就执行。
处理其他线程发送过来的任务
在上面这个线程只能处理来自线程自己的任务,如何实现来自其它线程的任务?
从上图可以看出,渲染主线程会频繁接收到来自于IO线程的一些任务,接收到这些任务之后,渲染进程就需 要着手处理,比如接收到资源加载完成的消息后,渲染进程就要着手进行DOM解析了;接收到鼠标点击的 消息后,渲染主线程就要开始执行相应的JavaScript脚本来处理该点击事件。
一个通用模式是使用消息队列消息队列
要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取 先进先出的特点
消息队列+事件循环
IO线程负责收集任务,然后转成一个个的可执行的任务,加入到消息队列,事件循环就一条条的执行。
构造消息队列
class TaskQueue{
public:
Task takeTask(); //取出队列头部的一个任务
void pushTask(Task task); //添加一个任务到队列尾部
};
TaskQueue task_queue;
void ProcessTask();
void MainThread(){
for(;;){
// 取出任务
Task task = task_queue.takeTask();
// 执行任务
ProcessTask(task);
}
}
// IO线程来添加任务
// Task clickTask;
// task_queue.pushTask(clickTask
由于是多个线程操作同一个消息队列,所以在添加任务和取出任务时还会加上一个同步锁,这块内容你也要注意下。
处理其他进程发送过来的任务 通过使用消息队列,我们实现了线程之间的消息通信。在Chrome中,跨进程之间的任务也是频繁发生的,那么如何处理其他进程发送过来的任务?你可以参考下图:
进程之间的通信通过IPC,然后给到IO线程组装成任务添加到消息队列。
从图中可以看出,渲染进程专⻔有一个IO线程用来接收其他进程传进来的消息渲染进程专⻔有一个IO线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续的步骤就和前面讲解的“处理其他线程发送的任务”一样了,这里就不再重复了。
任务是在IO线程组装的。
消息队列中的任务类型
Chrome 官方的任务类型 cs.chromium.org/chromium/sr…
如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript定时器等等。
除此之外,消息队列中还包含了很多与⻚面相关的事件,如JavaScript执行、解析DOM、样式计算、布局计算、CSS动画等。
以上这些事件都是在主线程中执行的,所以在编写Web应用时,你还需要衡量这些事件所占用的时⻓,并想办法解决单个任务占用主线程过久的问题。
如何安全退出?
当⻚面主线程执行完成之后,又该如何保证⻚面主线程能够安全退出呢?Chrome是这样解决的,确定要退出当前⻚面时,⻚面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。
如果设置了,那么就直接中断当前的所有任务,退出线程,你可以参考下面代码:
TaskQueue task_queue;
void ProcessTask();
bool keep_running = true;
void MainThread(){
for(;;){
Task task = task_queue.takeTask();
ProcessTask(task);
if(!keep_running) //如果设置了退出标志,那么直接退出线程循环
break;
}
}
⻚面使用单线程的缺点
通过上面的介绍,你应该清楚了,⻚面线程所有执行的任务都来自于消息队列。消息队列是“先进先出”的属性,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。鉴于这个属性,就有如下两个问题需要解决.
第一个问题是如何处理高优先级的任务 那该如何权衡效率效率和实时性实时性呢? 针对这种情况,微任务就应用而生了,下面我们来看看微任务是如何权衡效率和实时性的。
通常我们把消息队列中的任务称为宏任务宏任务,每个宏任务中都包含了一个微任务队列微任务队列,在执行宏任务的过程中,如果DOM有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此也就解决了执行效率的问题。
等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为DOM变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题。
第二个是如何解决单个任务执行时⻓过久的问题。
一个执行的太久其余的全部都要在队列中等待。
针对这种情况,JavaScript可以通过回调功能来规避这种问题,也就是让要执行的JavaScript任务滞后执行。至于浏览器是如何实现回调功能的,我们在后面的章节中再详细介绍。 或者跑个写成也行。 主要看设计。
实践:浏览器⻚面是如何运行的
有了上面的基础知识之后,我们最后来看看浏览器的⻚面是如何运行的。
你可以打开开发者工具,点击“Performance”标签,选择左上⻆的“startporfilingandloadpage”来记
录整个⻚面加载过程中的事件执行情况,如下图所示:
可以看出灰色的是一个个的任务,下面还有子任务,HTML Parse 解析DOM如果遇到JS停止了先跑JS。
总结
如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型。
要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版线程模型。
如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
如果其他进程想要发送任务给⻚面主线程,那么先通过IPC把任务发送给渲染进程的IO线程,IO线程再把任务发送给⻚面主线程。
消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务。
结合消息队列和事件循环,你认为微任务是什么?引入微任务能带来什么优势呢?
宏任务是开会分配的工作内容,微任务是工作过程中被临时安排的内容
主要有IO线程,用开负责和其它进程IPC通信的,然后主线程主要跑⻚面的!
V8是在主线程上执行的,因为dom操作啥的都是在主线程上执行的。
当然还有其它很多辅助线程,比如预解析DOM的线程,垃圾回收也有一些辅助线程。