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

IT技术 javascript node.js promise
2021-02-02 14:48:34

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

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

2个回答

将代码包装在Promise中(就像您所做的那样)不会使其成为非阻塞的。Promise 执行器函数(您传递给的回调new Promise(fn)是同步调用的,并且会阻塞,这就是为什么您会看到获取输出延迟的原因。

事实上,除了将其放入子进程、使用 WorkerThread、使用一些创建新 Javascript 线程的第三方库或使用用于线程的新实验性 node.js API。常规 node.js 以阻塞和单线程方式运行您的 Javascript,无论它是否包含在 Promise 中。

您可以使用诸如setTimeout()更改代码运行的“时间”之类的东西,但是无论何时运行,它仍然会阻塞(一旦它开始执行,其他任何东西都无法运行,直到它完成)。node.js 库中的异步操作都使用某种形式的底层原生代码,允许它们是异步的(或者它们只是使用其他 node.js 异步 API,它们本身使用原生代码实现)。

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

正如我上面所说,在 promise executor 函数中包装东西不会使它们异步。如果你想“改变”事物运行的时间(认为它们仍然是同步的),你可以使用 a 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);
    })
}

这将重新安排耗时的for循环稍后运行,并且可能“看起来”是非阻塞的,但它实际上仍然阻塞 - 它只是稍后运行。要使其真正无阻塞,您必须使用前面提到的技术之一将其从主 Javascript 线程中取出。

在 node.js 中创建实际非阻塞代码的方法:

  1. 在单独的子进程中运行它并在完成后获得异步通知。
  2. 在 node.js v11 中使用新的实验性工作线程
  3. 将您自己的本机代码插件编写到 node.js 并在您的实现中使用 libuv 线程或操作系统级线程(或其他操作系统级异步工具)。
  4. 建立在先前存在的异步 API 之上,并且没有在主线程中花费很长时间的您自己的代码。
对于 C++ 插件中的操作系统级线程,根据我的经验libuv非常好。这是我发现一个基本示例我个人使用它进行实时图像处理以在非阻塞线程中进行对象检测,然后 Node.js 将检测到的质心分发到连接的 TCP 客户端(使用数据驱动电机向检测到的对象移动)。
2021-03-25 14:48:34

promise 的 executor 函数是同步运行的,这就是你的代码阻塞主执行线程的原因。

为了不阻塞执行的主线程,您需要在执行长时间运行的任务时定期和协作地让出控制权。实际上,您需要将任务拆分为子任务,然后在事件循环的新滴答上协调子任务的运行。通过这种方式,您可以为其他任务(如渲染和响应用户输入)提供运行的机会。

您可以使用 promise API 编写自己的异步循环,也可以使用异步函数。异步函数可以暂停和恢复函数(重入),并向您隐藏大部分复杂性。

以下代码用于setTimeout将子任务移动到新的事件循环刻度上。当然,这可以概括,批处理可用于在任务进度和 UI 响应之间找到平衡;此解决方案中的批量大小仅为 1,因此进展缓慢。

最后:这类问题的真正解决方案可能是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>