消息队列和事件循环系统

主线程是怎么处理不同的任务

  • 使用单线程处理安排好的任务 —— 按顺序执行
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动画等。这些都是在主线程中执行。(开发时多考虑这些事件多占用的时长,想办法解决任务占用主线程过程久的问题)

    如此一来,我们可以这么处理:


    队列+循环
    1. 添加一个消息队列;
    2. IO线程产生的新任务添加到消息队列尾部;
    3. 渲染主线程会循环地从消息队列头部读取任务,执行任务。
// 构造一个队列, 不考虑队列的具体实现
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任务滞后执行。(浏览器是如何实现回调功能?)

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容