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

Posted

技术标签:

【中文标题】在 Node.js 中编写非阻塞函数的正确方法【英文标题】:Correct way to write a non-blocking function in Node.js 【发布时间】:2019-05-21 10:16:02 【问题描述】:

我写了一个返回 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

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

【问题讨论】:

好吧,如果你想真正测试“一段时间后”,你可以在你的 resolve(sum) 语句周围加上一个 setTimeout。 这行不通。您的代码只能获得一个线程。在承诺或超时中包装同步代码不会改变这一点。如果你想编写异步代码,你需要创建一个child process 相关:对于客户端,还有Web Worker API用于创建单独的线程任务。 【参考方案1】:

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

事实上,没有办法创建自己的非阻塞纯 javascript 代码(就像你所拥有的那样),除非将其放入子进程中,使用 WorkerThread,使用一些创建新线程的第三方库Javascript 或对线程使用新的实验性 node.js API。常规 node.js 以阻塞和单线程的方式运行您的 Javascript,无论它是否包含在 Promise 中。

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

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

正如我上面所说,将事物包装在 Promise executor 函数中不会使它们异步。如果你想“改变”事情运行的时间(认为它们仍然是同步的),你可以使用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 中创建实际非阻塞代码的方法:

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

【讨论】:

对于 C++ 插件中的操作系统级线程,根据我的经验,libuv 非常好。这是a basic example I found。我个人用它来实时进行图像处理,在非阻塞线程中进行对象检测,然后 Node.js 将检测到的质心分发给连接的 TCP 客户端(它使用数据驱动电机向检测到的对象移动)。 【参考方案2】:

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>

【讨论】:

以上是关于在 Node.js 中编写非阻塞函数的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

Node.js 非阻塞行为的新手查询

Node.js 回调函数 1) 阻塞 ,同步 2) 非阻塞 ,异步.

Node.js中的服务器架构&回调函数的非阻塞式应用

Node.js中的服务器架构&回调函数的非阻塞式应用

5Node.js 回调函数

Node.js:如何使用循环引用序列化大对象