将异步/等待阻塞线程node.js


74

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


Async / await具有同步行为,因此可以,它将阻塞当前各自的执行流,直到完成为止。
Lansana Camara'9

9
不,它不会阻塞线程。其他请求可以继续传入并在等待时得到处理。
凯文

我非常想澄清@KevinB的评论和这个问题:在等待的操作完成之前,是否不await'阻塞当前线程',而是允许其他线程继续执行?
Pac0'9

2
@ Pac0一个Node.JS进程只有一个线程,当您exec或时fork,将形成一个新进程,而不是一个新线程。
Nidhin David

如果您考虑一下,如果是这样,我们为什么需要async/await?我们可以只写阻塞代码。
Felix Kling

Answers:


145

async/await不会阻止整个解释器。node.js仍将所有Javascript作为单线程运行,即使某些代码在上等待async/await,其他事件仍可以运行其事件处理程序(因此不会阻塞node.js)。事件队列仍在为其他事件服务。实际上,这将是一个解决承诺的事件,该承诺将允许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()处理程序的编写并为代码提供同步的外观(尽管它并不是真正的同步)。最后,它是让您用更少的代码行编写异步代码的捷径。确实需要记住的是,任何可以拒绝的承诺都必须在其周围进行尝试/捕获才能捕获并处理该拒绝。

从逻辑上讲,您可以考虑await执行以下函数时,node.js在遇到关键字时会执行以下操作:

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

因此,尽管整个解释器没有阻塞(仍然可以处理其他Javascript事件),但是async包含该await语句的特定函数的执行被暂停,直到等待解决的诺言为止。要理解的重要步骤是上面的步骤5。当第一个await被点击时,该函数会立即返回一个未解决的promise和执行此函数后的代码(在promise被awaited解决之前)。出于这个原因,整个解释器不会被阻塞。执行继续。只有一个功能的内部才被挂起,直到承诺被解决。


2
我找到v8.dev/blog/fast-async作为获取此答案中解释的实际过程的详细信息的很好参考
Sasinda Rukshan

21

async/await提供传统then上对诺言的传统处理方式。也没有承诺,也async没有await创建新线程。

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

对该表达式求值后,该async函数将返回-它返回一个promise。然后,代码继续执行该函数调用之后的任何代码(同一线程),直到调用堆栈为空。

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

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

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


怎么样:stackoverflow.com/a/13823336/631527 No, it is not OK to use a blocking API call in a node server ,异步/等待版本和同步版本之间有什么区别?
工具包

2
真正等待发生的代码,而不是允许代码在发生之前在其他地方继续执行的代码,是同步的和阻塞的。我已经解释了async/await随着代码继续执行,该行为不会受到阻碍。
特里科特,

1
仅供参考,await除了语法糖以外,还有其他功能。例如,如果await在一个while()循环或一个for循环内,它将以某种方式暂停该循环,除非您以不使用该类型循环的完全不同的方式重写代码。因此,尽管许多花园品种的用途基本上都是语法糖,但它所具有的功能还不止这些。
jfriend00

7

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

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

本质上,async / await是围绕Promises进行的语言级包装,因此代码可以具有同步的“外观”


@Toolkit无法完全确定代码是否是非阻塞的,但如果它不返回承诺或提供回调,则有100%的可能性阻塞。
JLRishe

@Toolkit,您可以在运行时确定它-尝试与应用交互。节点阻塞或简单的内联测试-一个简单的例子是执行async内部无限循环的函数,while(1)然后尝试记录具有超时的内容setTimeout( ()=>{ console.log(..) } )-您将永远不会看到此日志
fider

1
@Nidhin David一个数据库调用被阻止了吗?例如,如果查询需要40毫秒才能执行,那么事件循环将被阻止40毫秒,对吗?
Sana.91年

@ Sana.91在Node.js中,不,它是异步的
Nidhin David

1
我很久以来在这个问题上见过的最佳答案。关于异步/等待有很多误解,因为大多数人都认为异步。表示无阻塞,但这仅当您在代码在备用线程中执行或利用执行该操作的系统调用时才如此。您自己编写的大多数代码(例如执行某些操作的for / loop)即使在async / await函数中也确实会阻塞。
加里

5

异步/等待会阻塞线程node.js吗?正如@Nidhin David所说,这取决于异步函数中包含的代码-数据库调用,网络调用,文件系统调用不会阻塞,但阻塞例如持续很长的时间/同时周期,JSON字符串化/解析和邪恶/脆弱的正则表达式(谷歌用于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);

该正则表达式字符串有什么问题。match(/ ^(a | a)+ $ /); ?
马里奥·鲁伊斯(Mario ruiz)

@MarioRuiz是邪恶的正则表达式,需要很长时间才能根据输入长度进行评估。执行时间根据输入长度呈指数增长,尝试使用它会发现输入很长时它永远不会结束。我也在javascript注释的文章顶部解释了该问题。一般请参见en.wikipedia.org/wiki/ReDoS
mikep '19

只是不明白为什么会这样?似乎非常简单... / ^(a | a)+ $ /
mario ruiz

您是说这些示例中的每个示例都将在调用/ test时阻塞线程,还是其中之一不会阻塞线程?
PrestonDocks

@PrestonDocks是的,如果/test因为代码string.match(/^(a|a)+$/)是同步的并且处理时间很长,所以调用请求,这4个示例中的每个示例都会阻塞主线程。我已经更新了答案以澄清这一点。
mikep

4

我刚听到一个“啊哈!” 片刻,以为我会继续下去。“ await”不会直接将控制权返回给JavaScript,而是将控制权返回给调用者。让我举例说明。这是一个使用回调的程序:

console.log("begin");
step1(() => console.log("step 1 handled"));
step2(() => console.log("step 2 handled"));
console.log("all steps started");

// ----------------------------------------------

function step1(func) {

console.log("starting step 1");
setTimeout(func, 10000);
} // step1()

// ----------------------------------------------

function step2(func) {

console.log("starting step 2");
setTimeout(func, 5000);
} // step2()

我们想要的行为是1)两个步骤都立即开始,以及2)当准备好要处理一个步骤(想象一个Ajax请求,但是这里我们只是等待一段时间)时,每个步骤的处理都会立即发生。

这里的“处理”代码是console.log(“步骤X已处理”)。该代码(在实际的应用程序中可能会很长,并且可能包含嵌套的等待),位于回调中,但我们希望它是函数中的顶级代码。

这是使用异步/等待的等效代码。请注意,我们必须创建一个sleep()函数,因为我们需要等待一个返回promise的函数:

let sleep = ms => new Promise((r, j)=>setTimeout(r, ms));

console.log("begin");
step1();
step2();
console.log("all steps started");

// ----------------------------------------------

async function step1() {

console.log("starting step 1");
await sleep(10000);
console.log("step 1 handled");
} // step1()

// ----------------------------------------------

async function step2() {

console.log("starting step 2");
await sleep(5000);
console.log("step 2 handled");
} // step2()

对我来说,重要的一点是,step1()中的await将控制权返回到主体代码,以便可以调用step2()来开始该步骤,而step2()中的await也返回主体代码,以使“所有步骤已开始”可以打印。有人主张您使用“ await Promise.all()”启动多个步骤,然后再使用结果(将出现在数组中)处理所有步骤。但是,这样做时,直到所有步骤都解决后,才会处理任何步骤。那不是理想的,似乎完全没有必要。


我相信您的意思是,如果所有这些初始步骤中的某些子集随后需要更多处理,那么等待并不是理想的选择。示例:您有诺言A,B和D;等待他们全部;用A和B产生承诺C; 等待C; 使用C和D来返回承诺E。示例中的问题是D的处理时间可能比A&B花费的时间更长,这延迟了C。Promise.all()综上所述,该示例非常有用,该示例仅显示了一种简单的用法。
检验一下

3

异步功能使我们能够像编写同步代码一样编写基于promise的代码,但不会阻塞执行线程。它通过事件循环异步运行。异步函数将始终返回值。使用异步仅意味着将返回一个承诺,如果未返回一个承诺,JavaScript会自动将其包装在带有其值的已解决的承诺中。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.