0
点赞
收藏
分享

微信扫一扫

浏览器原理 14 # 消息队列和事件循环


说明

浏览器工作原理与实践专栏学习笔记

概念

下面介绍来自百科

进程

进程(Process 一段程序的执行过程)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

线程

线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在 Unix System V 及 SunOS 中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。

一个进程可以有很多线程,每条线程并行执行不同的任务。

第一版:线程一次执行

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

例子:

void MainThread(){
int num1 = 1+2; //任务1
int num2 = 20/5; //任务2
int num3 = 7*8; //任务3
print("最终计算的值为:%d,%d,%d",num1,num2,num3) //任务4
}

浏览器原理 14 # 消息队列和事件循环_事件循环

按照顺序在线程中依次被执行;等所有任务执行完成之后,线程会自动退出。

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

在线程运行过程中处理新任务,所有的任务都是来自于线程内部的

要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制。

通过一个 for 循环语句来监听是否有新的任务:可以在线程运行过程中,等待用户输入的数字,等待过程中线程处于暂停状态,一旦接收到用户输入的信息,那么线程会被激活,然后执行相加运算,最后输出结果。

//GetInput
//等待用户从键盘输入一个数字,并返回该输入的数字
int GetInput(){
int input_number = 0;
cout<<"请输入一个数:";
cin>>input_number;
return input_number;
}

//主线程(Main Thread)
void MainThread(){
for(;;){
intfirst_num = GetInput()
int second_num = GetInput()
result_num = first_num + second_num;
print("最终计算的值为:%d",result_num)
}
}

在第一版的线程上做了两点改进:

  1. 引入了循环机制
  2. 引入了事件

浏览器原理 14 # 消息队列和事件循环_浏览器_02

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

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

其他线程是如何发送消息给渲染主线程?

浏览器原理 14 # 消息队列和事件循环_事件循环_03

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

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

浏览器原理 14 # 消息队列和事件循环_浏览器_04

改造方案:

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

浏览器原理 14 # 消息队列和事件循环_事件循环_05

1、构造一个队列

class TaskQueue{
public:
Task takeTask(); //取出队列头部的一个任务
void pushTask(Task task); //添加一个任务到队列尾部
};

2、改造主线程,让主线程从队列中读取任务

TaskQueue task_queue;
void ProcessTask();
void MainThread(){
for(;;){
Task task = task_queue.takeTask();
ProcessTask(task);
}
}

3、发送任务让主线程去执行

Task clickTask;
task_queue.pushTask(clickTask)

跨进程发送消息

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

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

渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续跟处理其他线程发送过来的任务一样。

浏览器原理 14 # 消息队列和事件循环_事件循环_06

消息队列中的任务类型

参考资料:​​【Chromium 的官方源码】​​

内部消息类型

  1. 输入事件(鼠标滚动、点击、移动)
  2. 微任务
  3. 文件读写
  4. WebSocket
  5. JavaScript 定时器等等

与页面相关的事件

  1. JavaScript 执行
  2. 解析 DOM
  3. 样式计算
  4. 布局计算
  5. CSS 动画等

如何保证页面主线程安全退出

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

消息队列机制并不是太灵活,为了适应效率和实时性,此时微任务就应用而生。通常把消息队列中的任务称为宏任务。每个宏任务中都包含了一个微任务队列

微任务是如何解决执行效率的问题?

在执行宏任务的过程中,如果 DOM 有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行。

微任务是如何解决实时性的问题?

等宏任务中的主要功能都直接完成之后,渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务。

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

单个任务执行时间过久:

浏览器原理 14 # 消息队列和事件循环_消息队列_07

JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。


举报

相关推荐

0 条评论