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

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

时间:2024-08-06 19:59:43浏览次数:16  
标签:resolve console log 单线程 编程 JS 任务 Promise result

如果某天有人问你,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

标签:resolve,console,log,单线程,编程,JS,任务,Promise,result
From: https://blog.csdn.net/2401_85195613/article/details/140929543

相关文章

  • 编程深水区之并发①:什么是并发编程
    并发编程是一种让程序能够执行多个任务的编程技术,多个任务的执行时间有重合,如交替执行、同时执行等。相对于传统的从上到下依次同步执行代码,我们也称并发编程为异步编程。目前,常见的并发模型主要有两种,一是多线程模型,二是单线程事件循环模型。一、多线程模型1、进程和线......
  • 编程深水区之并发④:Web多线程
    Node的灵感来源于Chrome,更是移植了V8引擎。在Node中能够实现的多线程,在Web环境中自然也可以。一、浏览器是多进程和多线程的复杂应用在本系列的第二章节,有提到现代浏览器是一个多进程和多线程的复杂应用。浏览器主进程统管全局,每个Tab页都会创建一个渲染子进程,同时还有G......
  • jsp“云味坊”购物网站9u653
    jsp“云味坊”购物网站9u653本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表项目功能用户,商品分类,商品品牌,热销商品,促销商品技术要求:   开发语言:JSP前端使用:HTML5,CSS,JSP动态网页技术后端使用Spr......
  • jsp“云课堂”在线教育系统的设计与开发87j57
    jsp“云课堂”在线教育系统的设计与开发87j57本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表项目功能学生,教师,课程分类,课程信息,课程练习,问题提问,在线沟通技术要求:   开发语言:JSP前端使用:HTML......
  • jsp“永梦”无人售货机系统管理的设计与实现bt3q1
    jsp“永梦”无人售货机系统管理的设计与实现bt3q1本系统(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。系统程序文件列表项目功能商品分类,补货员,维修员,用户,商品信息,设备报修,订单信息开题报告内容一、项目背景与意义随着科......
  • Spring Boot 中使用 JSON Schema 来校验复杂JSON数据
    JSON是我们编写API时候用于数据传递的常用格式,那么你是否知道JSONSchema呢?在数据交换领域,JSONSchema以其强大的标准化能力,为定义和规范JSON数据的结构与规则提供了有力支持。通过一系列精心设计的关键字,JSONSchema能够详尽地描述数据的各项属性。然而,仅凭JSONSchema......
  • 【编程语言】Delphi使用教程
    目录一、概述二、Delphi的开发环境三、Delphi基本功能3.1创建新项目3.2设计表单3.3编写代码3.4编译和运行3.5调试四、Delphi的高级的概念和技巧4.1使用组件和类4.2数据库操作4.3图形和多媒体4.4网络编程4.5调试和优化4.6 部署和分发4.7版本控制和......
  • Java基础编程练习题50题(没更新完会持续更新)
    1.古典问题有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第三个月后每个月又生一对兔子,假如兔子都不死,问每个月的兔子总数为多少?publicclassRabbit{publicstaticvoidmain(String[]args){intmonths=12;//假设计算12个月的情......
  • Java编程练习之集合
    1.产生10个1-100的随机数,并放到一个数组中,把数组中大于等于10的数字放到一个list集合中,并打印到控制台。publicstaticvoidmain(String[]args){//生成随机数Randomr=newRandom();//创建数组int[]arr=newint[10];/......
  • 异步编程和多线程有
    在C#中,多线程和异步编程是两个相关但不完全相同的概念。下面我会解释这两个概念的区别,并给出一些常见的问题及解答。多线程vs异步编程多线程:多线程指的是在一个进程中创建多个线程来并行执行任务。多线程可以用来处理计算密集型任务,充分利用多核处理器的计算能力。多......