跳到主要内容

3 篇博文 含有标签「异步编程」

查看所有标签

JavaScript 事件循环机制

· 阅读需 4 分钟

在 JavaScript 中,任务的执行顺序是基于事件循环(Event Loop)机制。理解这一机制对于编写高效、非阻塞的 JavaScript 代码至关重要。JavaScript 的执行模型可以分为以下几部分:

  1. 调用栈(Call Stack)

  2. 任务队列(Task Queue)

  3. 微任务队列(Microtask Queue)

  4. 事件循环(Event Loop)

调用栈(Call Stack)

调用栈是 JavaScript 引擎的一部分,用于跟踪函数调用的执行顺序。每当一个函数被调用时,它会被推入调用栈;函数执行完毕后,会从调用栈中弹出。调用栈是同步执行的,即一个函数必须执行完毕后,下一个函数才能执行。

任务队列(Task Queue)

任务队列用于存放异步任务的回调,如 setTimeoutsetInterval。当异步任务完成后,对应的回调函数会被放入任务队列中等待执行。

微任务队列(Microtask Queue)

微任务队列用于存放微任务,如 Promise 的回调函数和 MutationObserver 的回调。微任务具有更高的优先级,会在当前调用栈执行完毕后立即执行,优先于任务队列中的任务。

事件循环(Event Loop)

事件循环不断地检查调用栈是否为空,如果为空,它会检查微任务队列是否有任务需要执行。如果微任务队列为空,事件循环会从任务队列中取出第一个任务并执行。

执行顺序示例

通过以下代码示例,可以更清楚地了解 JavaScript 的任务执行顺序:

console.log("Start");

setTimeout(() => {
console.log("Timeout callback");
}, 0);

Promise.resolve().then(() => {
console.log("Promise callback");
});

console.log("End");

执行顺序如下:

  1. console.log("Start") 被推入调用栈并执行,输出 Start

  2. setTimeout 被推入调用栈并执行,注册一个回调函数,并设置 0 毫秒延迟。回调函数被放入任务队列。

  3. Promise.resolve().then 被推入调用栈并执行,注册一个微任务回调函数,并放入微任务队列。

  4. console.log("End") 被推入调用栈并执行,输出 End

  5. 调用栈清空后,事件循环检查微任务队列,发现有一个微任务。将微任务回调 console.log("Promise callback") 推入调用栈并执行,输出 Promise callback

  6. 微任务队列清空后,事件循环检查任务队列,发现有一个任务。将 setTimeout 的回调函数 console.log("Timeout callback") 推入调用栈并执行,输出 Timeout callback

最终输出顺序为:

Start
End
Promise callback
Timeout callback

总结

JavaScript 的任务执行顺序是基于事件循环、调用栈、微任务队列和任务队列的协同工作。理解这些机制有助于编写高效、非阻塞的代码,并避免常见的异步编程问题。

进程、线程与nodejs

· 阅读需 9 分钟

这篇文章是补齐我一些关于进程和线程的基础知识,以及他们和 nodejs 的关系。形式为 chatgpt 问答。

Q: 什么是计算机的进程和线程

在计算机科学中,进程(Process)和线程(Thread)是操作系统中用于执行程序的两个重要概念。

进程是指在计算机中正在运行的程序的实例。每个进程都有自己独立的内存空间、程序代码和数据。进程之间是相互独立的,它们不能直接访问其他进程的内存空间。进程有自己的程序计数器(Program Counter)、寄存器集合、打开的文件和其他系统资源等。操作系统通过调度算法来分配处理器时间给不同的进程,使它们能够并发执行。

线程是进程内的执行单元。一个进程可以包含多个线程,这些线程共享进程的内存空间和系统资源。线程之间可以更方便地进行通信和共享数据,因为它们可以直接读取和修改进程的内存。线程拥有自己的程序计数器、栈、寄存器集合,但它们共享进程的其他资源。由于线程共享了大部分资源,所以线程间的切换开销较小,可以实现更高效的并发执行。

与进程相比,线程具有以下优点:

  1. 线程的创建和销毁开销较小,速度更快。
  2. 线程之间的切换开销较小,响应时间更短。
  3. 线程共享进程的内存空间,可以更方便地进行数据共享和通信。

然而,线程也存在一些问题:

  1. 线程之间共享内存,需要进行同步和互斥操作,以避免数据竞争和不一致性。
  2. 一个线程的错误可能会影响到整个进程的稳定性。
  3. 多线程编程更加复杂,需要考虑线程同步、死锁等并发编程问题。

总结起来,进程和线程是操作系统中用于执行程序的两个基本概念。进程是程序的实例,具有独立的内存空间和系统资源;线程是进程内的执行单元,共享进程的内存空间。线程可以实现更高效的并发执行,但也需要注意并发编程带来的问题。

Q: 以 nodejs 处理网络请求为例,说明进程和线程的关系

在 Node.js 中,网络请求是通过事件驱动的非阻塞 I/O(Input/Output)模型来处理的。这个模型基于单线程的事件循环机制。

在 Node.js 中,一个进程通常指的是一个 Node.js 实例,它运行在操作系统的一个进程中。这个进程是由 Node.js 程序启动的,并拥有自己的内存空间、程序代码和系统资源。

在这个进程内部,Node.js 使用了单线程的事件循环机制来处理网络请求。事件循环是一个无限循环,不断地监听事件队列中的事件,并执行相应的回调函数。当有网络请求到达时,Node.js 将其包装成一个事件,并将其放入事件队列中,然后继续执行下一个事件。这样,Node.js 可以在处理一个请求的同时监听其他请求。

这里的关键点是,尽管 Node.js 使用单线程来处理事件循环,但它通过非阻塞 I/O 操作的方式,实现了对并发请求的高效处理。当一个请求需要进行网络 I/O 操作(如读取文件、发送 HTTP 请求等)时,Node.js 会将这个操作交给底层操作系统,并立即返回控制权。在等待 I/O 操作完成的期间,Node.js 可以继续处理其他请求,而不会被阻塞。

底层操作系统通常会使用多线程来处理具体的 I/O 操作,例如在读取文件时,可能会有一个线程负责从磁盘中读取数据,并将数据返回给 Node.js。这个过程对于 Node.js 来说是透明的,它只需要等待 I/O 操作完成,并通过事件机制得到通知。

因此,在 Node.js 中,进程代表了一个 Node.js 实例,而线程的管理是由底层操作系统负责的。Node.js 利用单线程的事件循环机制和底层操作系统的多线程 I/O 处理,实现了高效的网络请求处理。

Q: nodejs 进程只能利用到一个核心吗

在默认情况下,Node.js 是单线程的,它使用单个线程来执行 JavaScript 代码。这意味着一个 Node.js 进程只能利用到一个 CPU 核心。

然而,Node.js 通过使用事件驱动、非阻塞 I/O 和异步编程模型来实现高效的并发处理。它使用事件循环机制来处理并发请求,而不是为每个请求创建一个新的线程。这种设计使得单个 Node.js 进程可以处理大量的并发请求,而无需为每个请求分配一个单独的线程。

尽管 Node.js 的 JavaScript 代码执行是单线程的,但它可以利用多核处理器的优势。通过使用集群模块或者进程管理工具,可以创建多个 Node.js 进程并将请求分发给它们,从而实现并行处理。每个 Node.js 进程都可以运行在不同的 CPU 核心上,以充分利用多核处理器的计算能力。

例如,可以使用 Node.js 的 cluster 模块来创建一个主进程和多个工作进程,每个工作进程都可以运行在不同的 CPU 核心上,以并行处理请求。主进程负责接收和分发请求,而工作进程负责实际的请求处理。

另外,还可以使用一些第三方模块和工具,如 PM2、Nginx 等,来管理和扩展 Node.js 进程,以实现负载均衡和高可用性。

需要注意的是,多进程的部署需要考虑进程间的通信、数据同步和共享资源等问题。此外,由于每个进程都会占用一定的系统资源,过多的进程数量可能会导致系统负载增加。因此,在实际部署时,需要根据具体的场景和硬件资源配置来合理设置进程数量。

人人都能听懂的 javascript 异步机制

· 阅读需 7 分钟
Jason Lee

前言

同步异步一直是一个绕不过去的小问题,要么遇不上,要么遇上了似懂非懂。这篇文章力争从异步任务的底层需求出发,对 promise、async/await 进行一个详细的了解。

同步 or 异步?

16913918214981691391820745.png

上面这张图表示了两种模式的执行顺序区别,也是最根本的区别。

chatGPT 给出的定义如下:

异步任务是指在程序执行过程中,不会阻塞后续代码执行的任务。它们允许在后台执行,以便在等待长时间操作(例如网络请求、文件读写或计算密集型任务)完成时,程序能够继续执行其他任务,提高了程序的响应性和效率。 一个异步函数可以拆分为两个部分,一个是耗时部分,这部分时间内执行后面的代码来防止阻塞;另一个是执行回调任务部分,当耗时部分产生结果后,执行回调逻辑。

注:回调函数的定义是执行回调任务的一种形式,javascript 另外两种回调形式为 Promise,async/await。

setTimeout 函数

几乎所有讲解 js 异步机制的都会用 setTimeout 函数来举例子。它是一个最简单的执行异步任务的函数,意思是延迟一定时间后,执行一个回调函数。注意它有三个参数。

setTimeout(function, delay, [param1, param2, ...]);
  • function: 要执行的函数。
  • delay: 延迟的时间,单位为毫秒。

回调函数为什么被鄙视

解释完了同步和异步的差异以及异步的优势,下面说一下这篇文章要讨论的核心:假如目的是多个异步任务按顺序执行,应该如何书写易懂的代码?

上面说了异步任务的耗时部分结束后,要执行回调部分;如果用了回调函数的形式,且回调函数的执行结果要作为下一个异步任务的输入,那么就会有多个缩进,使得代码难以读懂,比如:

getUserInfo(function (userInfo) {
getUserOrders(userInfo.id, function (orders) {
orders.forEach(function (order) {
getOrderDetails(order.id, function (details) {
console.log("Order ID: " + order.id);
console.log("Order Details: " + details);
});
});
});
});

假设我们要依次执行三个异步操作:获取用户信息、获取用户订单列表,然后获取每个订单的详细信息。在上面的代码中,每个异步操作都依赖前一个操作的结果,因此它们被嵌套在一起。这会使代码变得难以理解。面对这样的嵌套回调,处理错误也会变得非常困难:你必须在“金字塔”的每一级处理错误,而不是在最高一级一次完成错误处理。

所以,现在的目标是:

  1. 减少嵌套,使得代码方便阅读和调试
  2. 在最高一级一次性处理错误

一句话总结:异步任务是让代码不会阻塞,而 promise 和 async/await 都是为了让多个异步任务按顺序执行的代码更易懂。

接下来看看 promise 和 async/await 是怎么让异步代码更易懂的?

promise(期约)

必须要说明的是,promise 的内容比较多,而且绕。不过,我们至少要记住它的表达形式:

function asyncTask(shouldResolve) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldResolve) {
resolve("Task completed successfully.");
} else {
reject(new Error("Task failed."));
}
}, 1000);
});
}

asyncTask()
.then((result) => {
console.log(result); // 输出: Task result
})
.catch((error) => {
console.error(error);
});

一个 Promise 实例被创建,并传入一个函数作为初始化参数。这个函数有两个参数,分别是 resolve 和 reject。resolve 含义是异步任务顺利执行,并返回一个值,这个值作为 resolve 的参数而调用。然后,then 方法里面传入的函数,其中的参数正是上面 resolve 种传入的参数。在这个例子里,"Task completed successfully."就是下面 then 里面的 result。另外,reject 指的是执行异步任务的过程中,发生了错误,这个错误经由 reject 函数,被下面的 catch 捕获。reject 抛出的错误只能被.catch 捕获,而不会被 try/catch 捕获。

我的理解是,resolve/reject 都是一种占位符。作用是当函数执行到这里,遇到他们俩,知道该往哪个方向走,并携带上参数。现在我们看看那两个目标是不是实现了。

  1. 减少嵌套:如果有进一步的的异步任务,可以放在 then 里面执行,有几层,就写几个 then,避免了嵌套。
  2. 一次性处理错误:最下面的.catch 会捕获.then 链中任意一步发生的错误(reject)。

async/await

同样的,先给出形式。

function asyncTask() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("Async task completed");
resolve("Task result");
}, 1000);
});
}

async function runTask() {
try {
const result = await asyncTask();
console.log(result); // 输出: Task result
} catch (error) {
console.error(error);
}
}

runTask();

异步函数是基于 Promise 的语法糖,使得异步任务的写法向同步任务看齐。这个例子中,asyncTask 函数显式返回了一个 Promise 对象,实际上,这个函数可以返回非 Promise 对象,只是会被 await 隐式地转化为 Promise 对象。await 字面意思包含暂停,也就是说,异步函数执行到 await 会暂停,等待事件循环队列下一次轮到自己执行。