web-dev-qa-db-ja.com

async / awaitはスレッドnode.jsをブロックします

async/awaitがnode.js関数で使用されると、コードの次の行を実行するまでnode.jsスレッドをブロックしますか?

39
rajesh_pudota

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()ハンドラーの記述を保存して、コードに同期的な外観を与えることができます(実際には同期的ではありません)。最終的には、より少ないコード行で非同期コードを記述できるようにする略記法です。ただし、拒否できるプロミスには、その拒否をキャッチして処理するために、その周囲のどこかにtry/catchが必要であることに注意する必要があります。

論理的には、node.jsが関数を実行するときにawaitキーワードに遭遇したときの処理を次のように考えることができます。

  1. 関数呼び出しが行われます
  2. インタープリターは、関数がasyncとして宣言されていることを確認します。これは、常にプロミスを返すことを意味します。
  3. インタプリタは関数の実行を開始します。
  4. awaitキーワードを検出すると、待機中のプロミスが解決されるまで、その関数の実行を一時停止します。
  5. 次に、関数は未解決のpromiseを返します。
  6. この時点で、インタプリタは、関数呼び出しの後に来るものをすべて実行し続けます(通常、fn().then()の後に他のコード行が続きます)。 promiseがまだ解決されていないため、.then()ハンドラーはまだ実行されていません。
  7. ある時点で、このJavaScriptのシーケンスは終了し、制御をインタープリターに戻します。
  8. インタプリタは、イベントキューから他のイベントを自由に処理できるようになりました。 awaitキーワードに遭遇した元の関数呼び出しはまだ中断されていますが、他のイベントを処理できるようになりました。
  9. 将来のある時点で、待っていた最初の約束は解決されます。それがイベントキューで処理される時間になると、以前に中断された関数は、awaitの後の行で実行を続けます。 awaitステートメントがさらにある場合、そのプロミスが解決されるまで、関数の実行は再び中断されます。
  10. 最終的に、関数はreturnステートメントに到達するか、関数本体の最後に到達します。 return xxxステートメントがある場合、xxxが評価され、その結果は、このasync関数が既に返した約束の解決された値になります。関数の実行が完了し、以前に返された約束が解決されました。
  11. これにより、.then()ハンドラーは、この関数が以前に返されて呼び出されたという約束に関連付けられます。
  12. これらの.then()ハンドラーが実行された後、このasync関数のジョブは最終的に完了します。

そのため、インタープリター全体はブロックしませんが(他のJavaScriptイベントは引き続きサービスできます)、asyncステートメントを含む特定のawait関数の実行は、待機中のプロミスが解決されるまで中断されました。理解することが重要なのは、上記のステップ5です。最初のawaitがヒットすると、この関数が実行された後(awaitedであるpromiseが解決される前に)、関数は未解決のpromiseとコードを直ちに返します。このため、インタープリター全体がブロックされません。実行は継続されます。約束が解決されるまで、1つの関数の内部のみが中断されます。

77
jfriend00

async/awaitは、約束のthen呼び出しの単なる構文上の砂糖です。 Promiseもasyncawaitも新しいスレッドを作成しません。

awaitが実行されると、それに続く式が同期的に評価されます。それは約束であるべきですが、そうでない場合は、await Promise.resolve(expression)を持っているかのように1つにラップされます。

その式が評価されると、async関数は戻ります-約束を返します。その後、呼び出しスタックが空になるまで、その関数呼び出し(同じスレッド)に続くコードが何であれ、コード実行が継続されます。

ある時点で、awaitに対して評価された約束が解決されます。これにより、マイクロタスクがマイクロタスクキューに追加されます。 JavaScriptエンジンが現在のタスクで実行することはない場合、マイクロタスクキューの次のイベントを消費します。このマイクロタスクは解決されたプロミスを含むため、async関数の前の実行状態を復元し、awaitの後に続くものを続行します。

関数は、同様の動作で他のawaitステートメントを実行できますが、関数は元の呼び出し元に戻らなくなりました(その呼び出しは最初のawaitで既に処理されていたため)呼び出しスタックを空のままにし、JavaScriptエンジンを残してマイクロタスクとタスクキューを処理します。

これはすべて同じスレッドで発生します。

11
trincot

Async/await内に含まれるコードがブロックされていない限り、たとえばdb呼び出し、ネットワーク呼び出し、ファイルシステム呼び出しなど、ブロックされません。

ただし、async/await内に含まれるコードがブロックされている場合、無限ループ、画像処理などのCPU集中タスクなど、Node.jsプロセス全体がブロックされます。

本質的にasync/awaitはPromiseの言語レベルのラッパーであり、コードに同期的な「ルックアンドフィール」を持たせることができます。

4
Nidhin David

Async/awaitはスレッドnode.jsをブロックしますか? @Nidhin Davidが言ったように、非同期関数の内部にあるコードに依存します-db呼び出し、ネットワーク呼び出し、ファイルシステム呼び出しはブロックされませんが、ブロックは例えば長いfor/whileサイクル、JSON stringify/parseおよびevil/vulnerable正規表現(google ReDoS攻撃の場合)。


この最初の例では、メインノードスレッドが期待どおりにブロックされ、他のリクエスト/クライアントは処理できません。

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 / に対して多くの並列リクエストを実行でき、高速になります。次に、1つの遅いリクエストだけを実行します http:// localhost:8080/test / そして他のリクエストはありません( http:// localhost:8080 / で速いリクエストでも)低速(ブロック)リクエストが終了するまで配信されます。


この2番目の例ではプロミスを使用していますが、メインノードスレッドをブロックしているため、他のリクエスト/クライアントは処理できません。

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

この3番目の例では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);

4番目の例では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);
3
mikep

「あは!」瞬間と私はそれを渡すと思った。 "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処理」)です。そのコード(実際のアプリケーションでは非常に長くなる可能性があり、ネストされた待機を含む可能性があります)はコールバックにありますが、関数の最上位コードであることが望ましいです。

Async/awaitを使用した同等のコードを次に示します。 promiseを返す関数で待機する必要があるため、sleep()関数を作成する必要があることに注意してください。

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()」を使用して複数のステップを開始し、その後、結果(配列に表示される)を使用してすべてのステップを処理することを唱える人もいます。ただし、それを行うと、すべてのステップが解決されるまでステップは処理されません。これは理想的ではなく、まったく不要なようです。

2
user1738579