消息队列和事件循环

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 消息队列和事件循环

每个渲染进程都有一个主线程,主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度系统就是我们今天要讲的消息队列和事件循环系统

使用单线程处理安排好的任务

我们把所有任务代码按照顺序写进主线程里,等线程执行时,这些任务会按照顺序在线程中依次被执行;等所有任务执行完成之后,线程会自动退出。

image.png

第一版:线程的一次执行

在线程运行过程中处理新任务

但并不是所有的任务都是在执行之前统一安排好的,大部分情况下,新的任务是在线程运行过程中产生的。比如在线程执行过程中,又接收到了一个新的任务要求计算“10+2”,那上面那种方式就无法处理这种情况了。

要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制。我们可以通过一个 for 循环语句来监听是否有新的任务,如下面的示例代码:

//GetInput
// 等待用户从键盘输入一个数字,并返回该输入的数字
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 循环语句,线程会一直循环执行。
  • 第二点是引入了事件,可以在线程运行过程中,等待用户输入的数字,等待过程中线程处于暂停状态,一旦接收到用户输入的信息,那么线程会被激活,然后执行相加运算,最后输出结果。

通过引入事件循环机制,就可以让该线程“活”起来了,我们每次输入两个数字,都会打印出两数字相加的结果,你可以结合下图来参考下这个改进版的线程:

image.png

第二版:在线程中引入事件循环

处理其他线程发送过来的任务

在第二版的线程模型中,所有的任务都是来自于线程内部的,如果另外一个线程想让主线程执行一个任务,利用第二版的线程模型是无法做到的。

那下面我们就来看看其他线程是如何发送消息给渲染主线程的

image.png

渲染进程线程之间发送任务

从上图可以看出,渲染主线程会频繁接收到来自于 IO 线程的一些任务,接收到这些任务之后,渲染进程就需要着手处理,比如接收到资源加载完成的消息后,渲染进程就要着手进行 DOM 解析了;接收到鼠标点击的消息后,渲染主线程就要开始执行相应的 JavaScript 脚本来处理该点击事件。

如何设计好一个线程模型,能让其能够接收其他线程发送的消息呢?

一个通用模式是使用消息队列

什么是消息队列

image.png

消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取

改造方案如下图所示:

image.png

第三版线程模型:队列 + 循环

从上图可以看出,我们的改造可以分为下面三个步骤:

  1. 添加一个消息队列;
  2. IO 线程中产生的新任务添加进消息队列尾部;
  3. 渲染主线程会循环地从消息队列头部中读取任务,执行任务。

处理其他进程发送过来的任务

image.png

跨进程发送消息

从图中可以看出,渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续的步骤就和前面讲解的“处理其他线程发送的任务”一样了。

消息队列中的任务类型

消息队列中的任务类型有哪些。

这里面包含了很多内部消息类型,如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等等。

消息队列中还包含了很多与页面相关的事件,如 JavaScript 执行、解析 DOM、样式计算、布局计算、CSS 动画等。

以上这些事件都是在主线程中执行的,在编写 Web 应用时,需要衡量这些事件所占用的时长,并想办法解决单个任务占用主线程过久的问题。

当页面主线程执行完成之后,又该如何保证页面主线程能够安全退出呢?

Chrome 是这样解决的,确定要退出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。

如果设置了,那么就直接中断当前的所有任务,退出线程。

页面使用单线程的缺点

页面线程的一些特征

页面线程所有执行的任务都来自于消息队列。消息队列是“先进先出”的属性,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。

鉴于这个属性,就有如下两个问题需要解决。

第一个问题是如何处理高优先级的任务。

比如一个典型的场景是监控 DOM 节点的变化情况(节点的插入、修改、删除等动态变化),然后根据这些变化来处理相应的业务逻辑。

观察者模式

一个通用的设计的是,利用 JavaScript 设计一套监听接口,当变化发生时,渲染引擎同步调用这些接口,这是一个典型的观察者模式

不过这个模式有个问题,因为 DOM 变化非常频繁,如果每次发生变化的时候,都直接调用相应的 JavaScript 接口,那么这个当前的任务执行时间会被拉长,从而导致执行效率的下降

异步的消息事件

如果将这些 DOM 变化做成异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了。

如果 DOM 发生变化,采用同步通知的方式,会影响当前任务的执行效率;如果采用异步方式,又会影响到监控的实时性

那该如何权衡效率和实时性呢?

针对这种情况,微任务就应用而生了

微任务是如何权衡效率和实时性的

通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,也就解决了执行效率的问题。

等宏任务中的主要功能都直接完成之后,这时候,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为 DOM 变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题

第二个是如何解决单个任务执行时长过久的问题。

因为所有的任务都是在单线程中执行的,所以每次只能执行一个任务,而其他任务就都处于等待状态。如果其中一个任务执行时间过久,那么下一个任务就要等待很长时间。

image.png

单个任务执行时间过久

从图中你可以看到,如果在执行动画过程中,其中有个 JavaScript 任务因执行时间过久,占用了动画单帧的时间,这样会给用户制造了卡顿的感觉,这当然是极不好的用户体验。针对这种情况,JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。

实践:浏览器页面是如何运行的

浏览器的页面是如何运行的

打开开发者工具,点击“Performance”标签,选择左上角的“start porfiling and load page”来记录整个页面加载过程中的事件执行情况:

image.png

Performance 页面

从图中可以看出,我们点击展开了 Main 这个项目,其记录了主线程执行过程中的所有任务。图中灰色的就是一个个任务,每个任务下面还有子任务,其中的 Parse HTML 任务,是把 HTML 解析为 DOM 的任务。值得注意的是,在执行 Parse HTML 的时候,如果遇到 JavaScript 脚本,那么会暂停当前的 HTML 解析而去执行 JavaScript 脚本。

总结

  • 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型。
  • 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版线程模型。
  • 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
  • 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。
  • 消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务。

思考时间

今天给你留的思考题是:结合消息队列和事件循环,你认为微任务是什么?引入微任务能带来什么优势呢?

目录
相关文章
|
3月前
|
消息中间件 监控
异步处理和消息队列的结合使用
异步处理和消息队列的结合使用
|
5月前
|
消息中间件 监控 JavaScript
异步处理和消息队列
异步处理和消息队列
|
消息中间件 存储 数据库
消息队列有什么用
通过异步处理提高系统性能(减少响应所需时间) 削峰/限流 降低系统耦合性。
103 0
|
消息中间件 存储 监控
消息队列与任务队列的区别
消息队列和任务队列是我们在软件系统中常常遇到的概念。尽管它们的名字相似,但实际上它们有不同的用途和工作原理。本文将介绍消息队列和任务队列之间的区别。
606 0
|
消息中间件 存储 容灾
优秀的消息队列
优秀的消息队列
69 1
|
消息中间件 监控 大数据
高并发设计系列-消息队列篇
高并发设计系列-消息队列篇
|
消息中间件 缓存 容灾
22. 为什么需要消息队列?使用消息队列有什么好处?
22. 为什么需要消息队列?使用消息队列有什么好处?
188 0
22. 为什么需要消息队列?使用消息队列有什么好处?
|
消息中间件 Java 数据库
消息队列(五)
消息队列(五)
170 0
消息队列(五)
|
消息中间件 存储 中间件
消息队列(四)
消息队列(四)
209 0
消息队列(四)
|
消息中间件 SQL 关系型数据库
消息队列
消息队列
241 0

热门文章

最新文章

下一篇
开通oss服务