async/await 会阻塞一个线程 node.js

IT技术 javascript node.js async-await
2021-03-12 23:13:29

async/await在一个的node.js函数使用时,它会阻塞的node.js线程,直到它执行代码的下一行?

6个回答

async/await不会阻塞整个解释器。node.js 仍然将所有 Javascript 作为单线程运行,即使某些代码正在等待async/await,其他事件仍然可以运行它们的事件处理程序(因此 node.js 不会被阻止)。事件队列仍在为其他事件服务。事实上,这将是一个解决Promise的事件,该Promise将允许await停止等待并运行以下代码。

像这样的代码:

await foo();            // foo is an async function that returns a promise
console.log("hello");

类似于:

foo().then(() => {
    console.log("hello");
});

因此,await只需将该范围内的以下代码放入一个不可见的.then()处理程序中,其他所有内容的工作方式与实际使用.then()处理程序编写的几乎相同

因此,await允许您保存.then()处理程序的编写并为代码提供同步外观(尽管它不是真正同步)。最后,它是一种速记,可让您用更少的代码行编写异步代码。确实需要记住,任何可以拒绝的Promise都必须在它周围的某个地方有一个 try/catch 来捕获和处理该拒绝。

从逻辑上讲,你可以想象 node.jsawait在执行函数时遇到关键字时会做的事情如下:

  1. 进行了函数调用
  2. 解释器看到函数被声明为async这意味着它总是会返回一个Promise。
  3. 解释器开始执行函数。
  4. 当它遇到一个await关键字时,它会暂停该函数的进一步执行,直到正在等待的Promise得到解决。
  5. 然后该函数返回一个未解决的Promise。
  6. 此时,解释器继续执行函数调用之后的任何内容(通常 afn().then()后跟其他代码行)。.then(),因为Promise尚未解决处理程序尚未执行。
  7. 在某些时候,这个 Javascript 序列完成并将控制权返回给解释器。
  8. 解释器现在可以自由地为事件队列中的其他事件提供服务。遇到await关键字的原始函数调用仍然暂停,但现在可以处理其他事件。
  9. 在未来的某个时刻,正在等待的原始Promise得到解决。当需要在事件队列中处理它时,先前挂起的函数会在await. 如果还有更多await语句,则函数执行将再次暂停,直到该Promise解决为止。
  10. 最终,函数命中一个return语句或到达函数体的末尾。如果有return xxx语句,则xxx计算 并且其结果成为该async函数已经返回的Promise的解析值该函数现在已完成执行,并且它先前返回的Promise已得到解决。
  11. 这将导致任何.then()附加到此函数先前返回的Promise的处理程序被调用。
  12. 在这些.then()处理程序运行之后,这个async函数的工作终于完成了。

因此,虽然整个解释器不会阻塞(其他 Javascript 事件仍然可以服务),但async包含该await语句的特定函数的执行被暂停,直到正在等待的Promise得到解决。重要的是要理解上面的第 5 步。当第一个await被命中时,该函数立即返回一个未解析的Promise和代码,在该函数执行后(在Promise被awaited解析之前)。正是因为这个原因,整个解释器没有被阻塞。执行继续。只有一个函数的内部被挂起,直到一个 promise 被解决。

我发现v8.dev/blog/fast-async作为获取此答案中解释的实际过程的详细信息的一个很好的参考
2021-04-20 23:13:29

async/await为您传统then上通过 Promise 调用所做的事情提供另一种方式也Promise,也没有async也没有await创建新的线程。

await被执行时,它后面的表达被同步评估。该表达式的计算结果应为 promise,但如果不是,则将其包装为一个,就像您拥有await Promise.resolve(expression).

一旦该表达式被评估,该async函数就会返回——它返回一个Promise。然后代码继续执行该函数调用(同一线程)之后的任何代码,直到调用堆栈为空。

在某个时候,Promise - 被评估await- 将解决。这会将微任务放入微任务队列中。当 JavaScript 引擎在当前任务中无事可做时,它将消耗微任务队列(FIFO)中的下一个事件。由于此微任务涉及已解决的Promise,因此它将恢复async函数的先前执行状态并继续执行await.

该函数可能会执行其他await具有类似行为的语句,尽管该函数现在不再返回到最初调用它的位置(因为该调用已经用第一个 处理过await),它只是返回使调用堆栈为空,并保留 JavaScript引擎来处理微任务和任务队列。

所有这些都发生在同一个线程中。

真正等待某事发生而不允许代码在发生之前在其他地方继续执行的代码是同步的,并且是阻塞的我已经解释了async/await的行为不会在代码继续执行时阻塞。
2021-04-25 23:13:29
仅供参考,await除了语法糖之外还有一些功能。例如,如果awaitwhile()循环或for循环内,它将以一种方式挂起该循环,如果不以不使用该类型循环的完全不同的方式重写代码,则无法完成该循环。因此,虽然许多园艺品种的用途本质上是语法糖,但它的功能不止于此。
2021-05-02 23:13:29
这个怎么样:stackoverflow.com/a/13823336/631527,async/await No, it is not OK to use a blocking API call in a node server 和同步版本有什么区别?
2021-05-09 23:13:29

只要包含在 async/await 中的代码是非阻塞的,它就不会阻塞,例如数据库调用、网络调用、文件系统调用。

但是如果包含在 async/await 中的代码是阻塞的,那么它就会阻塞整个 Node.js 进程,例如无限循环、图像处理等 CPU 密集型任务等。

本质上,async/await 是 Promises 的语言级包装器,因此代码可以具有同步的“外观和感觉”

@Nidhin David 是不是数据库调用被阻塞了?就像如果一个查询需要 40 毫秒来执行,那么事件循环将被阻塞 40 毫秒,对吗?
2021-04-19 23:13:29
@Toolkit 没有办法完全确定代码是否是非阻塞的,但是如果它不返回Promise或提供回调,则它有 100% 的机会阻塞。
2021-04-21 23:13:29
@Sana.91 在 Node.js 中,不,它是异步的
2021-05-02 23:13:29
@Toolkit 您可以在运行时确定这一点 - 尝试与应用程序交互。节点阻塞或只是简单的内联测试 - 简单的例子是在async内部执行带有无限循环的函数,while(1)然后尝试用超时记录一些东西setTimeout( ()=>{ console.log(..) } )- 你永远不会看到这个日志
2021-05-09 23:13:29
我在很长一段时间内看到的关于这个主题的最佳答案。关于 async/await 有很多误解,因为大多数人认为 async. 意味着非阻塞,但这仅在您指定的代码在备用线程中执行或使用系统调用时才成立。您自己编写的大多数代码 - 例如执行某些操作的 for/loop,即使在 async/await 函数中也确实会阻塞。
2021-05-13 23:13:29

async/await 会阻塞线程 node.js 吗?正如@Nidhin David 所说,这取决于您在异步函数中的代码 - db 调用、网络调用、文件系统调用不会阻塞,但阻塞是例如 long for/while 循环、JSON stringify/parse 和 evil/vulnerable 正则表达式(谷歌用于 ReDoS 攻击)。如果/test请求被调用,以下四个示例中的每一个都会阻塞主线程,因为代码string.match(/^(a|a)+$/)是同步的并且需要很长时间来处理。


第一个示例将按预期阻塞主节点线程,并且无法为其他请求/客户端提供服务。

var http = require('http');

// This regexp takes to long (if your PC runs it fast, try to add some more "a" to the start of string).
// With each "a" added time to complete is always doubled.
// On my PC 27 times of "a" takes 2,5 seconds (when I enter 28 times "a" it takes 5 seconds).
// https://en.wikipedia.org/wiki/ReDoS
function evilRegExp() {
    var string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaab';
    string.match(/^(a|a)+$/);
}

// Request to http://localhost:8080/ wil be served quickly - without evilRegExp() but request to
// http://localhost:8080/test/ will be slow and will also block any other fast request to http://localhost:8080/
http.createServer(function (req, res) {
    console.log("request", req.url);

    if (req.url.indexOf('test') != -1) {
      console.log('runing evilRegExp()');
      evilRegExp();
    }

    res.write('Done');
    res.end();
}).listen(8080);

您可以对http://localhost:8080/运行许多并行请求,而且速度会很快。然后只运行一个慢速请求http://localhost:8080/test/并且在慢速(阻塞)请求结束之前不会提供其他请求(即使是那些在http://localhost:8080/处快速的请求)。


第二个示例使用了 Promise,但它仍会阻塞主节点线程,并且无法为其他请求/客户端提供服务。

var http = require('http');

function evilRegExp() {
    return new Promise(resolve => {
        var string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaab';
        string.match(/^(a|a)+$/);
        resolve();
    });
}

http.createServer(function (req, res) {
      console.log("request", req.url);

    if (req.url.indexOf('test') != -1) {
      console.log('runing evilRegExp()');
      evilRegExp();
    }

    res.write('Done');
    res.end();

}).listen(8080);

第三个示例使用 async+await 但它也是阻塞的(async+await 与原生 Promise 相同)。

var http = require('http');

async function evilRegExp() {
    var string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaab';
    string.match(/^(a|a)+$/);
    resolve();
}

http.createServer(function (req, res) {
      console.log("request", req.url);

    if (req.url.indexOf('test') != -1) {
      console.log('runing evilRegExp()');
      await evilRegExp();
    }

    res.write('Done');
    res.end();

}).listen(8080);

第四个示例使用 setTimeout() 导致慢速请求似乎立即得到服务(浏览器很快“完成”)但它也被阻塞并且任何其他快速请求将等待直到 evilRegExp() 结束。

var http = require('http');

function evilRegExp() {
    var string = 'aaaaaaaaaaaaaaaaaaaaaaaaaaab';
    string.match(/^(a|a)+$/);
}

http.createServer(function (req, res) {
      console.log("request", req.url);

    if (req.url.indexOf('test') != -1) {
      console.log('runing evilRegExp()');
      setTimeout(function() { evilRegExp(); }, 0);
    }

    res.write('Done');
    res.end();

}).listen(8080);
你是说如果 /test 被调用,这些例子中的每个人都会阻塞线程,还是其中之一不会阻塞线程?
2021-04-25 23:13:29
那个正则表达式 string.match(/^(a|a)+$/); 有什么问题??
2021-05-06 23:13:29
@MarioRuiz 这是一个邪恶的正则表达式,根据输入长度需要很长时间来评估。执行时间根据输入长度呈指数增长。尝试使用它,您会清楚地看到当输入很长时它永远不会结束。我还在我的帖子顶部的 javascript 评论中解释了它。一般参见en.wikipedia.org/wiki/ReDoS
2021-05-07 23:13:29
@PrestonDocks 是的,如果/test请求被调用,这 4 个示例中的每个示例都会阻塞主线程,因为代码string.match(/^(a|a)+$/)是同步的并且需要很长时间来处理。我已经更新了我的答案以澄清这一点。
2021-05-07 23:13:29
只是不明白为什么会这样?看起来很简单... /^(a|a)+$/
2021-05-14 23:13:29

异步函数使我们能够编写基于 Promise 的代码,就像它是同步的一样,但不会阻塞执行线程。它通过事件循环异步运行。异步函数将始终返回一个值。使用 async 只是意味着将返回一个Promise,如果没有返回一个Promise,JavaScript 会自动将它包装在一个已解析的Promise中,并带有它的值。

在 Medium 上查找文章。 如何在 JavaScript 中使用 Async Await。