TypeScript

TypeScript の async/await を理解する その2 async/await

前の2つのエントリは、このエントリを調べたいための準備だったのです。

やっと本編のasync / await について書くことができます。

Promise で非同期処理の順序性を保証してみる。

Promise によって、非同期処理の書きやすさが一段アップしましたが、async/wait の登場によってさらに素晴らしいものになりました。まずは、Promise で非同期のコード書いてみます。

function waitAndAnswer(message:string) : Promise<any> {
    console.log("Wait for 3 second.");
    return new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(`You said ${message}`);
        resolve();
    }, 3000);
    });
}

console.log("step 1");
waitAndAnswer("hi");
console.log("step 2");
waitAndAnswer("Konnnichiwa");
console.log("end");

これを実行すると

step 1
Wait for 3 second.
step 2
Wait for 3 second.
end
You said hi
You said Konnnichiwa

これを順序性を保証するように書き換えてみましょう。

先ほどのコードのの、waitAndAnswer() メソッドの呼び出し部分を次のように変えます。

console.log("step 1");
waitAndAnswer("hi").then(()=> {
    console.log("step 2");
    return waitAndAnswer("konnichiwa"); // NOTE: Promise を戻している
}).then(() => {
    console.log("end");
});
step 1
Wait for 3 second.
You said hi
step 2
Wait for 3 second.
You said konnichiwa
end

waitAndAnswer()の戻り値は、Promise であり、Promise の resolve が呼ばれない限り、then メソッドは呼ばれません。ちなみに NOTE: の行に書いていますが、この行を単純に waitAndAnswer("konnichiwa"); にしてしまうと、return 句がないので、最終行を実行した時点で、おそらく暗黙にreturn;` が実行された結果、それがPromise にラップされて戻されるので、結果はこうなりました。

step 1
Wait for 3 second.
You said hi
step 2
Wait for 3 second.
end
You said konnichiwa

async/await による書き換え

前回のブログで、Promise の resolve と reject 及び、then と catch メソッドの関係がわかってきたところでさらに async/await に進みます。これは、C# をやられている方にはとてもおなじみの非同期処理のキーワードです。

これらのキーワードで上記のプログラムを書き換えていきます。

function waitAndAnswer(message:string) : Promise<any> {
    console.log("Wait for 3 second.");
    return new Promise((resolve, reject) => {
    setTimeout(() => {
        console.log(`You said ${message}`);
        resolve();
    }, 3000);
    });
}

async function exec() {
    console.log("step 1");
    await waitAndAnswer("hi");
    console.log("step 2");
    await waitAndAnswer("konnichiwa");
    console.log("end");    
}

exec();

実行結果

step 1
Wait for 3 second.
You said hi
step 2
Wait for 3 second.
You said konnichiwa
end

とこのように、とっても簡単になりました。await 句を、メソッド呼び出しの前に記述すると、結果が戻ってくるのを待ちます。ただ、await 句は、async キーワードで非同期と宣言したメソッドの中にしか書けませんので、メソッドを分けて、async キーワードを追加した、exec() メソッドを作成し、最下行で、exec()を呼んだ時点で別スレッドとして、exec()が起動されます。exec()メソッド実行のスレッドの内部から、さらにWaitAndAnswerのメソッドが別スレッドで実行されますが、await 句があることで、処理が返ってくるまで待ち合わせます。これはどういう仕組みでしょうか?

async/await の動作原理

上記のコードは、下記の javascript ファイルに変換されます。

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
function waitAndAnswers(message) {
    console.log("Wait for 3 second.");
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`You said ${message}`);
            resolve();
        }, 3000);
    });
}
function exec() {
    return __awaiter(this, void 0, void 0, function* () {
        console.log("step 1");
        yield waitAndAnswers("hi");
        console.log("step 2");
        yield waitAndAnswers("konnichiwa");
        console.log("end");
    });
}
exec();

なんかごちゃごちゃしててよーわからんので、最初の__awaiterを自分でもある程度分かるように書き換えて単純にしてみた。

"use strict";

var awaiter = function(thisArg, generator) {
    return new Promise((resolve, reject) => {
        let gen = generator.apply(thisArg);
        function fulfilled(value) { try { step(gen.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(gen["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new Promise(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step(gen.next());
    });
}

function waitAndAnswers(message) {
    console.log("Wait for 3 second.");
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`You said ${message}`);
            resolve();
        }, 3000);
    });
}
function exec() {
    return awaiter(this, function* () {
        console.log("step 1");
        yield waitAndAnswers("hi");
        console.log("step 2");
        yield waitAndAnswers("konnichiwa");
        console.log("end");
    });
}
exec();

実行結果

step 1
Wait for 3 second.
You said hi
step 2
Wait for 3 second.
You said konnichiwa
end

これは、Generator と、Promise の合わせ技になっている。基本的な構造は、await キーワードをつけた非同期実行が、yield によって、ストップする。await がついたメソッドは、Promise を返却するものである必要があり、Promise が返却されるのを期待されている。yield は、Generator の next()が呼ばれると、次の行に制御を移すが、next()メソッドは、Promise の resolve が呼ばれた時、つまり、非同期処理(ここでは、waitAndAnswers)が完了した時点で、呼ばれる。
だから、非同期処理(waitAndAnswers)の処理が終わるまでは、yeild部分で処理が止まるため、非同期処理であっても、順次実行が行われる。また、本質的にコールバックなので、メインスレッドがブロックされるわけではない。

疑問点

ここまでコードを書いたりしておいて、自分の中で未解決の疑問点が2つある。

なぜ同期型のメソッドは推奨されないのか?

今回の待ち合わせ対象の非同期メソッドは、waitAndAnswersで、単に待っているだけだが、TypeScript だと、fs.writeFileSync()というメソッドがあって、同期処理をしてくれる。この場合、

async function write() {
             :
      fs.writeFileSync(name, data);
             :
}

write();

とするケースと

async function writeFile(name, data): Promise<any> {
   fs.writeFile(name, data, (err)=> {
       if (err) {
         reject(err);
       } else {
         resolve();
       }
   }
}

async function write() {
             :
      await writeFile(name, data);
             :
}

write();

みたいにするのではスレッドの動き的にどう違うんだろう。メインスレッドが、write()のスレッドを起動して、write()を実行して、最初のケースだと同期なので、write()のスレッドはブロックされる。awaitを使ってケースだと、write()のスレッドはブロックされないけど、待ち合わせることになるから。大きな違いを感じない。メインスレッドはいずれにしてもロックされないから。
 これは師匠に確認してわかりました。私は非同期処理はスレッドが立ち上がって実行されると勘違いしていましたが、そうではないようです。

非同期I/O待ち

この記事が最高にわかりやすかったです。非同期処理ごとにスレッドが起動するのではなく、いくつか上がっているスレッドに対して、割り当てられる感じです。そうであるならば、スレッドがロックされてしまうと、大幅なパフォーマンスロスになりますね。

awaiter の挙動

先のオリジナルの__awaiter で理解できなかった部分としてこのコードがあります。

function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected);

この時点で、既に非同期処理がおわっていれば、resolve 、そうでなければ、新たにPromise のオブジェクトを作ってfullfill と rejected の関数を割り当てる。と理解できるのですが、

new P(function (resolve) { resolve(result.value); })

これは何をやっているんでしょうか? これだと、即座にresolve を呼び出す Promise
のように見えます。IO待ちなどでは、この時点で、result.value は帰ってきていない可能性も高いでしょう。

私も先生に聞いてみますが、もし皆様もお分かりならご教授いただけるとうれしいです。
回答をいただいたらブログをアップデートします。