主线程是怎么处理不同的任务
- 使用单线程处理安排好的任务 —— 按顺序执行
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
}
- 在线程运行过程中,接受并处理新任务 —— 事件循环机制
// getInput
int getInput() {
int input_num = 0;
cout<<"请输入一个数: ";
cin>>input_number;
return input_number;
}
// 主线程
void mainThread() {
for(;;) {
int first_num = getInput();
int second_num = GetInput();
result_num = first_num + second_num;
print("最终 计算的值为: %d", result_num);
}
}
做的改进:
第一,引入循环机制,线程中添加了for循环语句,线程会一直循环执行。
第二,引入了事件,在线程进行过程中,等待用户输入的数字,等待过程中,线程处于暂停状态,直到接收到用户的输入,会被激活并执行相加运算,输出结果。
-
处理其他线程发送来的任务
首先要了解其他线程是怎么给渲染主线程发送消息的:
渲染进程中线程间发送任务
IO线程会频繁的向渲染主线程发送任务,主线程接收到任务就需要一一处理。接受到资源加载完成的消息后,渲染进程就要着手DOM解析;接到鼠标点击的消息后,渲染进程就要执行相应的JS脚本来处理点击事件。这样就需要有一个“东西”来接收其他线程发送来的消息——通用模式是使用消息队列。
消息队列消息队列:一种数据结构,特点是先进先出。可以存放要执行的任务,任务类型有很多,比如事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JS定时器等;与页面相关的,如JS执行、解析DOM、样式计算、布局计算、CSS动画等。这些都是在主线程中执行。(开发时多考虑这些事件多占用的时长,想办法解决任务占用主线程过程久的问题)
如此一来,我们可以这么处理:
队列+循环- 添加一个消息队列;
- 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);
}
}
// 其他线程发送任务让主进程执行, 将任务添加到消息队列中
Task clickTask;
task_queue.pushTask(clickTask)
由于是多个线程操作同一个消息队列,所以在添加任务和取出任务时会添加上一个同步锁。
-
处理其他进程发送过来的任务
跨进程发送消息
IO线程用来接收其他进程传进来的消息,接收到消息后会把它们组装成任务发送给渲染主线程。后续步骤和处理线程发送的任务一致。
页面主线程安全退出
(Chrome做法)设置一个退出标志的变量,当确定要退出当前页面,则将变量设置为退出。每次执行完一个任务时判断是否需要退出。
TaskQueue task_queue;
void ProcessTask();
bool keep_running = true;
void MainThread(){
for(;;){
Task task = task_queue.takeTask();
ProcessTask(task);
if(!keep_running) //如果设置了退出标志,那么直接退出线程循环
break;
}
}
页面使用单线程的缺点
因为页面线程要执行的任务都来自消息队列,队列有“先进先出”的属性,导致放入队列中的任务需要等前面的任务都执行完,才会被执行。这就需要考虑两个问题:
1. 如何处理高优先级的任务
比如说,监控DOM节点变化情况,来处理相应的业务逻辑。这可以利用JS设计一套监听接口实现,当发生变化时,渲染引擎同步调用这些接口。但是当DOM变化非常频繁时,每次都直接调用相应的JS接口,那么当前的任务执行时间会被拉长,从而导致执行效率下降。
如果将DOM变化做出异步的消息事件,添加到队列尾部,又会影响监控的实时性(加入时可能前面就有很多任务在排队了)。
为了权衡上面的效率和实时性,微任务应用而生。
通常而言,消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列。在执行宏任务过程中,如果DOM变化,就会将该变化添加到微任务列表中,这样就不影响宏任务的继续执行(解决执行效率)。
等宏任务中的主要功能都完成后,先执行当前宏任务中的微任务(解决实时性),再去执行下一个宏任务。
2. 如何解决单个任务执行时长过久的问题
因为所有的任务都是在单线程中执行,每次只能执行一个任务,其他的会处于等待状态。那么要是有一个任务执行时间太久,这会使下个任务等待很长时间。
比如说,执行一个动画过程中,其中有个JS任务执行时间过久,占用了动画单帧的时间,会给用户造成卡顿感觉。这种情况,JS 可以通过回调功能来规避这种问题,即让执行的JS任务滞后执行。(浏览器是如何实现回调功能?)