提问人:nosbor 提问时间:12/21/2018 最后编辑:Ben Astonnosbor 更新时间:10/26/2023 访问量:5383
在 Node.js 中编写非阻塞函数的正确方法
Correct way to write a non-blocking function in Node.js
问:
我编写了一个简单的函数,它返回 Promise,因此应该是非阻塞的(在我看来)。不幸的是,该程序看起来停止等待 Promise 完成。我不确定这里会出什么问题。
function longRunningFunc(val, mod) {
return new Promise((resolve, reject) => {
sum = 0;
for (var i = 0; i < 100000; i++) {
for (var j = 0; j < val; j++) {
sum += i + j % mod
}
}
resolve(sum)
})
}
console.log("before")
longRunningFunc(1000, 3).then((res) => {
console.log("Result: " + res)
})
console.log("after")
输出如预期所示:
before // delay before printing below lines
after
Result: 5000049900000
但是程序在打印第二行和第三行之前会等待。您能解释一下首先打印“之前”和“之后”然后(一段时间后)结果的正确方法是什么吗?
答:
将代码包装在 promise 中(就像你所做的那样)并不能使其非阻塞。Promise 执行器函数(您传递给的回调是同步调用的,并且会阻塞,这就是您在获取输出时看到延迟的原因。new Promise(fn)
事实上,除了将其放入子进程中,使用WorkerThread,使用一些创建新Javascript线程的第三方库或使用新的实验性node.js API之外,没有办法创建自己的非阻塞的纯Javascript代码(就像你所拥有的一样)。常规 node.js 以阻塞和单线程的形式运行您的 Javascript,无论它是否包装在 promise 中。
你可以使用诸如更改代码运行“时间”之类的东西,但每当它运行时,它仍然会阻塞(一旦它开始执行,在它完成之前,其他任何东西都不能运行)。node.js 库中的异步操作都使用某种形式的底层本机代码,允许它们是异步的(或者它们只使用其他本身使用本机代码实现的 node.js 异步 API)。setTimeout()
但是程序在打印第二行和第三行之前会等待。您能解释一下首先打印“之前”和“之后”然后(一段时间后)结果的正确方法是什么吗?
正如我上面所说,将内容包装在 promise 执行器函数中不会使它们异步。如果你想“改变”事物运行的时间(认为它们仍然是同步的),你可以使用 ,但这并没有真正使任何事情不阻塞,它只是让它稍后运行(运行时仍然阻塞)。setTimeout()
因此,您可以这样做:
function longRunningFunc(val, mod) {
return new Promise((resolve, reject) => {
setTimeout(() => {
sum = 0;
for (var i = 0; i < 100000; i++) {
for (var j = 0; j < val; j++) {
sum += i + j % mod
}
}
resolve(sum)
}, 10);
})
}
这将重新安排耗时的循环稍后运行,并且可能“看起来”是非阻塞的,但实际上它仍然阻塞 - 它只是稍后运行。为了使它真正无阻塞,您必须使用前面提到的技术之一将其从主 Javascript 线程中取出。for
在 node.js 中创建实际非阻塞代码的方法:
- 在单独的子进程中运行它,并在完成后收到异步通知。
- 在 node.js v11 中使用新的实验性工作线程
- 将您自己的本机代码插件编写到 node.js,并在您的实现(或其他操作系统级别的异步工具)中使用 libuv 线程或操作系统级别的线程。
- 在以前存在的异步 API 之上构建,并且没有在主线程中花费很长时间的自己的代码。
评论
promise 的执行器函数是同步运行的,这就是您的代码阻塞执行主线程的原因。
为了不阻塞执行的主线程,您需要在执行长时间运行的任务时定期合作地让出控制权。实际上,您需要将任务拆分为子任务,然后在事件循环的新周期上协调子任务的运行。通过这种方式,您可以为其他任务(如呈现和响应用户输入)提供运行机会。
您可以使用 promise API 编写自己的异步循环,也可以使用异步函数。异步函数支持函数的暂停和恢复(重入),并隐藏大部分复杂性。
以下代码用于将子任务移动到新的事件循环刻度上。当然,这可以概括,批处理可用于在任务进度和 UI 响应能力之间找到平衡;此解决方案中的批大小仅为 1,因此进度很慢。setTimeout
最后:这类问题的真正解决方案可能是 Worker。
const $ = document.querySelector.bind(document)
const BIG_NUMBER = 1000
let count = 0
// Note that this could also use requestIdleCallback or requestAnimationFrame
const tick = (fn) => new Promise((resolve) => setTimeout(() => resolve(fn), 5))
async function longRunningTask(){
while (count++ < BIG_NUMBER) await tick()
console.log(`A big number of loops done.`)
}
console.log(`*** STARTING ***`)
longRunningTask().then(() => console.log(`*** COMPLETED ***`))
$('button').onclick = () => $('#output').innerHTML += `Current count is: ${count}<br/>`
* {
font-size: 16pt;
color: gray;
padding: 15px;
}
<button>Click me to see that the UI is still responsive.</button>
<div id="output"></div>
正如 [@jfriend00] 所解释的,执行器函数不会排队到微任务的 promise 队列中。要真正理解这句话,我建议观看 Nodejs 的 Event Loop 上的这个视频系列。事件循环从第 42 - 48 集开始处理,但我建议也观看前面的剧集。提到的微任务队列似乎是 v8 的一部分,而不是 libuv 事件循环的一部分,这就是为什么以下内容适用于 Nodejs 和浏览器 JS。fn
new Promise(fn)
只有传递给后续语句的函数才会排队到微任务的 promise 队列中。
因此,我们可以实例化一个 Promise 并立即解析它,并将 的长计算放入以下内容中。then
sum
then
function longRunningFunc(val, mod) {
return new Promise((resolve, reject) => {
// Inside promise's executor function
// Synchronously executed from v8's callstack
// Not queued into the microtask's promise queue!
console.log("Inside Promise's executor function")
resolve()
}).then(() => {
// Queued into the microtask's promise queue!
// Thus, logged some time after "after"
// Executed on v8's callstack as soon as popped from microtask's promise queue
console.log("Start computing long sum")
sum = 0;
for (var i = 0; i < 100000; i++) {
for (var j = 0; j < val; j++) {
sum += i + j % mod
}
}
console.log("Finished computing long sum")
return sum
})
}
console.log("before")
longRunningFunc(1000, 3).then((res) => {
console.log("Result: " + res)
})
console.log("after")
输出:
before Inside Promise's executor function after Start computing long sum [ ... some delay ... ] Finished computing long sum Result: 5000049900000
备注:立即解决承诺的更短方法是使用Promise.resolve()
function longRunningFunc(val, mod) {
return Promise.resolve().then(() => {
console.log("Start computing long sum")
sum = 0;
for (var i = 0; i < 100000; i++) {
for (var j = 0; j < val; j++) {
sum += i + j % mod
}
}
console.log("Finished computing long sum")
return sum
})
}
评论