在 Node.js 中编写非阻塞函数的正确方法

Correct way to write a non-blocking function in Node.js

提问人:nosbor 提问时间:12/21/2018 最后编辑:Ben Astonnosbor 更新时间:10/26/2023 访问量:5383

问:

我编写了一个简单的函数,它返回 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

但是程序在打印第二行和第三行之前会等待。您能解释一下首先打印“之前”和“之后”然后(一段时间后)结果的正确方法是什么吗?

JavaScript 节点.js 承诺

评论

0赞 Taplar 12/21/2018
好吧,如果你想真正测试“一段时间后”,你可以在你的 resolve(sum) 语句周围放置一个 setTimeout。
2赞 Mark 12/21/2018
这是行不通的。您的代码只有一个线程。将同步代码包装在 promise 或 timeout 中不会改变这一点。如果要编写异步代码,则需要创建一个子进程
1赞 Patrick Roberts 12/21/2018
相关:对于客户端,还有用于创建单独的线程任务的 Web Worker API

答:

18赞 jfriend00 12/21/2018 #1

将代码包装在 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 中创建实际非阻塞代码的方法:

  1. 在单独的子进程中运行它,并在完成后收到异步通知。
  2. 在 node.js v11 中使用新的实验性工作线程
  3. 将您自己的本机代码插件编写到 node.js,并在您的实现(或其他操作系统级别的异步工具)中使用 libuv 线程或操作系统级别的线程。
  4. 在以前存在的异步 API 之上构建,并且没有在主线程中花费很长时间的自己的代码。

评论

2赞 Patrick Roberts 12/21/2018
对于 C++ 插件中的操作系统级别线程,根据我的经验,libuv 非常好。这是我找到的一个基本例子。我个人使用它进行实时图像处理,以在非阻塞线程中进行对象检测,然后 Node.js 将检测到的质心分发到连接的 TCP 客户端(使用数据驱动电机向检测到的对象移动)。
1赞 Ben Aston 5/6/2020 #2

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>

0赞 dhjn 10/25/2023 #3

正如 [@jfriend00] 所解释的,执行器函数不会排队到微任务的 promise 队列中。要真正理解这句话,我建议观看 Nodejs 的 Event Loop 上的这个视频系列。事件循环从第 42 - 48 集开始处理,但我建议也观看前面的剧集。提到的微任务队列似乎是 v8 的一部分,而不是 libuv 事件循环的一部分,这就是为什么以下内容适用于 Nodejs 和浏览器 JS。fnnew Promise(fn)

只有传递给后续语句的函数才会排队到微任务的 promise 队列中。 因此,我们可以实例化一个 Promise 并立即解析它,并将 的长计算放入以下内容中。thensumthen

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
    })
}