编程深水区之并发②:JS的单线程事件循环机制

如果某天有人问你,Node.js是单线程还是多线程,你如何回答?

一、单线程并发原理

我们以处理Web请求为例,来看看Node在处理并发请求时,究竟发生了什么。
Node启动Web服务器后,创建主线程(只有一个)。当有一个阻塞请求过来时,主线程不会发生阻塞,而是继续处理其它代码或请求。如果阻塞事件中有异步任务,如网络请求、文件IO等,将交由底层的libuv处理,libuv利用自身的工作线程池或直接利用操作系统的IO机制(如epoll、kqueue 和 IOCP),执行异步任务,当libuv完成异步任务后,将完成任务的事件和回调函数推入事件循环的任务队列中。Node的事件循环机制会不断轮询任务队列,它将在某次轮询中,处理这个事件,并执行对应的回调函数。
JS的确是运行在单线程上,但Node.js的底层架构,则是通过 libuv 库,使用多线程来处理耗时的异步任务,一般情况下,libuv的工作线程池是自动管理的(默认4个,可以配置,但不建议配置太多,会增加线程开销)。除此之外,Node还支持手动创建多线程或子进程,实现更灵活的并发解决方案,详见后续章节。

二、事件循环机制实战

2.1 任务队列的执行顺序

JS引擎在处理代码时,将代码分别放入同步任务队列、微任务队列和宏任务队列。事件循环机制,不断轮询队列任务,按以下顺序执行任务:

  • 执行同步队列中的所有代码,按代码顺序执行(含方法调用的跳转)。
  • 执行微任务队列中的所有任务,按先进先出的规则执行,直到清空为止。
  • 按先进先出的规则,执行宏任务队列中的第一个宏任务,宏任务中可能有同步代码、微任务或宏任务,同步代码立即被执行,微任务被放入微任务队列、宏任务被放入宏任务队列;接着执行被放入微任务队列中的所有微任务,执行完成后,再取出后面的宏任务,依次循环执行。
  • 微任务队列中的任务,也有可能存在同步代码、微任务和宏任务,同步代码被立即执行,微任务被放入微任务队列,宏任务被放入宏任务队列。
  • 其实代码开始执行时,如script标签开始,就可以看成是第一个宏任务。

如果只是JS引擎,不涉及Node或浏览器环境,以上任务,无论是同步任务、微任务或者宏任务,都由JS引擎的单线程来执行。JS异步操作的特点,是不会发生阻塞,即单线程一直在执行任务,而不会在某个任务上等待。

2.2 如何区别微任务和宏任务

  • 微任务包括:promise.then()、proces.nextTick()、mutationObserver()
  • 宏任务包括:setTimeout、setInterval、setImmediate、script

2.3 代码解释任务队列的执行顺序

//代码的执行顺序为: 1->5->3->4->2
console.log('1'); // 同步任务

setTimeout(function() {
    console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(function() {
    console.log('3'); // 微任务①
}).then(function() {
    console.log('4'); // 微任务②
});

console.log('5'); // 同步任务

//注意Promise的同步和异步区别,then中的代码才会放到微任务中
new Promise((resolve,reject)=>{
    console.log("这是在同步代码里");
}).then(()=>{
    console.log("这是在异步代码里")
})
    

三、增加DOM渲染后情况变复杂了

3.1 Web中任务队列的执行顺序

<div id="app">初始内容</div> <!--【1】UI渲染-->

<!--宏任务①-->
<script>
  console.log('同步代码开始'); // 【2】同步代码

  setTimeout(() => { //宏任务②
    console.log('setTimeout'); //【6】宏任务
    document.getElementById('app').textContent = 'setTimeout 修改内容';//【7】UI渲染
  }, 1000); // 增加1秒的延迟

  Promise.resolve().then(() => {
    //微任务①
    console.log('Promise 1'); // 【4】微任务
    document.getElementById('app').textContent = 'Promise 1 修改内容'; //【5】UI渲染
    
    //微任务①中的宏任务③【8】
    return new Promise(resolve => setTimeout(resolve, 2000)); 
  }).then(() => {
    console.log('Promise 2'); //【8-1】微任务
    document.getElementById('app').textContent = 'Promise 2 修改内容'; //【8-2】UI渲染
  });

  console.log('同步代码结束'); // 【3】同步代码
</script>

以上代码的执行顺序为,还算正常:

控制台/JS线程DOM/UI线程
【1】初始内容 - 几乎看不到,脚本执行的很快
【2】同步代码开始
【3】同步代码结束
【4】Promise 1【5】Promise 1 修改内容
【6】setTimeout【7】setTimeout 修改内容
【8-1】Promise 2【8-2】Promise 2 修改内容

但是,如果将【return new Promise(resolve => setTimeout(resolve, 2000)); 】,修改为200毫秒,执行顺序会不太一样,大家可以在浏览器中执行试一下。

3.2 为什么会发生执行顺序的意外?

现代浏览器的进程和线程机制复杂很多,Chrome浏览器不仅是多进程,还是多线程。进程方面,主要有浏览器主进程-统管全局,渲染子进程-每个Tab页都会创建一个子进程,GPU子进程,插件子进程等。如下图所示:
Snipaste_2024-06-24_09-26-58.png

而在渲染子进程内部,实际上是多线程的。有执行JS代码的线程(单线程事件循环)、有渲染UI的线程、也有负责网络请求的线程。以上代码之所以有差异,原因在于JS线程和UI渲染线程的运行机制。DOM渲染,会在微任务队列空闲时执行(指在一次事件循环中的清空微任务队列的所有微任务时),但它不会打断JS线程,DOM开始渲染时,JS线程以非阻塞姿态继续执行。

四、补充说一下Promise和async/await

4.1 创建和调用Promise

//1、创建一个Promise============================================
//Promise构造函数接受一个函数,创建Promise时立即执行
//这个函数的方法体是一般是异步操作,参数resolve和reject也是函数
//调用resolve(data)时,表示异步操作成功,返回data数据
//调用reject(errorMsg),表示异步操作失败,返回错误信息
//*注意:new Promise(()=>{此处代码同步执行})
let promise = new Promise((resolve, reject) => {
  // 异步操作
  setTimeout(() => {
    let data = 'Async operation result';
    // 成功时调用 resolve,将结果传递出去
    resolve(data);
    // 失败时调用 reject,传递错误信息
    // reject('Error occurred');
  }, 2000);
});

/*
Promise构造函数接受一个执行函数,该函数在创建 Promise 实例时立即执行。
异步操作完成后,调用 resolve 表示操作成功,并传递结果;调用reject表示操作失败,并传递错误
使用 then 方法处理操作成功的情况,使用 catch 方法处理操作失败的情况
*/

//2、调用Promise==============================================
//then方法处理操作成功的情况,接收结果数据,并执行方法体中的回调
//catch方法处理操作失败的情况,接收错误信息,并执行方法体中的回调
//*注意:then/catch(()=>{此处代码放入微任务队列})
promise.then((result) => {
  console.log('Promise resolved:', result);
}).catch((error) => {
  console.error('Promise rejected:', error);
});

4.2 Promise的链式调用

//每个 then 方法返回的是一个新的 Promise
//每个步骤依赖于前一个步骤的结果

//调用fetchData(),返回Promise实例
function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            let data = 'Async operation result';
            resolve(data);
        }, 2000);
    });
}
//then方法返回Promise实例
//then中return出去的结果,可以在下一个链式then中接收,类似resolve
fetchData()
    .then((result) => {
        console.log('First promise resolved:', result);
        // 返回一个新的 Promise
        return 'Second promise result';
    })
    .then((result) => {
        console.log('Second promise resolved:', result);
        // 返回一个值,会被包装为 resolved Promise
        return 'Third promise result';
    })
    .then((result) => {
        console.log('Third promise resolved:', result);
    })
    .catch((error) => {
        console.error('Promise rejected:', error);
    });

4.3 执行多个Promise实例

//可以将多个Promise实例,包装成一个新的Promise实例
//就像是多个任务赛跑,分为全部完成、任意一个完成和先完成几个情况

//情况1:全部任务都fulfilled,才fulfilled,任意一个rejected,则rejected
//生成一个Promise实例的数组
const promises = [2, 3, 5, 7, 11, 13].map(item=>{
  //假设getJSON方法返回promise
  return getJSON('/post/' + item + ".json"); 
});
//开始跑任务
Promise.all(promises) //生成一个新的Promise实例
  .then(posts=>{
  // ...posts是每个Promise的result组成的数组
}).catch(reason=>{
  // ...
});

//情况2:任意一个任务fulfilled,则fulfilled;全部rejected,才rejected
Promise.any(promises)

//情况3:任意一个任务返回结果,则跟着返回结果,包括fulfilled和rejected
Promise.any(promises)

//情况4:所有任务状态都返回结果,才返回结果,包括fulfilled和rejected
Promise.any(promises)

4.4 async/await是Promise的语法糖

JS的async/await在语法上和C# 很像,但两者本质不一样。JS的async/await只是Promise的语法糖,本质还是单线程。

// async 函数定义
async function fetchData() {
    // await 表达式会暂停函数执行,直到 Promise 解决为止,并返回解决值
    let result = await new Promise((resolve, reject) => {
        setTimeout(() => {
            let data = 'Async operation result';
            resolve(data);
        }, 2000);
    });

    // 在 async 函数中,可以像同步代码一样处理结果
    console.log('Async operation completed:', result);
    return result;
}

// 调用 async 函数
fetchData()
    .then((result) => {
        console.log('Async function resolved:', result);
    })
    .catch((error) => {
        console.error('Async function rejected:', error);
    });


*这是一个系列文章,将全面介绍多线程、用户态协程和单线程事件循环机制,建议收藏、点赞哦!
*你在并发编程过程中碰到了哪些难题?欢迎评论区交流~~~


我是functionMC > function MyClass(){…}
C#/TS/鸿蒙/AI等技术问题,以及如何写Bug、防脱发、送外卖等高深问题,都可以私信提问哦!

image.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值