GUIアプリケーション(Webアプリを含む)や,通信を使うアプリケーション(クライアント,サーバ両方)では,普通複数の処理が同時に動く。なぜなら
- GUIアプリケーションでは,クリックされた時に完全に画面が止まってしまったら良くない。
- 通信をしている間に,通信と関係ないものも止まってしまうのは良くない。
- サーバの場合,レスポンスを返すのに時間がかかっている間に,次のリクエストを受け付けられないと,同時に複数のリクエストをさばく能力が非常に落ちる。
例えば,Railsの場合だと,ApacheなどのWebサーバがプロセスなどのOSの仕組みを使ってくれるので,これはあまり問題にならない。Androidアプリケーション開発などでも,スレッドなどを簡単に使える仕組みを基礎で学ぶ(やらない人も多いが…)。しかし,JSの場合は1つのプロセスの中で全てが動くようになっているので,ある処理が時間をとってしまうとそこで他の処理が全部止まってしまう。これをブロッキングという。ブロッキングが起きてしまうと↑の問題が全部起こる。ブロッキングが起こらないのが非同期処理だとざっくり言うことができる。
JSでは,関数単位で物事が動く。このため,ブラウザではsetTimeout/setImmediate,node.jsではprocess.nextTickなど,「一旦プロセスを他の処理にバトンタッチしてあげて,あとで関数の残りを実行する」ことで,ジャグリングのように複数の処理を同時にやることができる。Promiseなどでは,その仕組を組み込んである。これをイベントループという。このループが小刻みに進むと,いろいろなイベントを同時にこなすことができるようになる。
さて,JSで問題なのが,「どんなに頑張っても,ブロッキングが起きてしまう箇所が1箇所でもあると,そこで処理が止まってしまう」ということである。ブロッキングを徹底的に排除しなければならない。なので,最近のライブラリは,非同期処理を内部に組み込んで,強制的に使わせるようにしている。これが,JSで非同期処理でつまづきまくる理由である。
これ,一言では言うの難しいな。時系列を追っていこう。
いにしえの時代,2014年までは,どこでバトンタッチするかを自分で決める必要があり,実現方法も非常に原始的なものであった。
apigateway.getRestApis({}, function(err, result) {
if(err) {
console.log(err);
} else {
console.log(result);
}
});
console.log('hoge');
これは手元にあったAWS SDKの例である。このコードの特徴として,
- getRestApisに関数を渡している
- getRestApis自体は,リクエストを送信したら,「レスポンスが帰ってきたというイベントが来たら,与えられた関数を実行する」ということを宣言して,一旦終了する。このため,リクエストを送信したらconsole.log('hoge');が即座に実行される。
ここで重要なのが,従来のプログラミング言語のように「Aを実行したら次にB,その次にC」のように順番に実行する書き方ができないということである。つまり,「レスポンスが帰ってきたというイベントが来たら,与えられた関数を実行する」という非同期のラインと,console.log('hoge');を実行する同期のラインが2つ枝分かれしてしまうのである。この問題を解決するために,昔ながらのJSでは,「コールバック渡し」というスタイルを使ってきた。つまり,「処理が終わったら実行する関数」を関数の引数として与えるのだ。これは関数プログラミングの考え方で,実はJSは難しい。要は,時間がかかるものはおいといて,次の処理にさっさと回すということがわかっていればよい。
コールバック渡しの悪い点は,非同期処理が何度もつながっていく場合である。例えば,通信を何度かやる場合を考えてみよう。
apigateway.getRestApis({}, function(err, result) {
if(err) {
console.log(err);
} else {
apigateway.getRestApis({}, function(err, result) {
if(err) {
console.log(err);
else {
apigateway.getRestApis({}, function(err, result) {
...
非常に辛い。8段階目くらいになると読めない。この辛さを解決するために,Promise,asyncなどが出てきたといってもよい。
ということで,非同期処理をスムーズに書けるようにするためにいろいろな方法が考案された。
- Promise: 「何か中に処理が入っていて,成功したらデータが,失敗したらエラーが返ってくるんだけど,いつ返ってくるかはわからない。帰ってきたかどうか,成功(resolve)か失敗(reject)かを中で管理していて,返ってきた場合の処理(then)も指定できるオブジェクト」。これも手元にあったAWS SDKを引用。
var AWS = require('aws-sdk');
function getRestApisTest() {
return new Promise((resolve, reject) => {
AWS.config.update({ region: 'us-west-2' });
apigateway = new AWS.APIGateway();
apigateway.getRestApis({}, function(err, result) {
if(err) {
reject(err);
} else {
resolve(result);
}
});
});
}
getRestApisTest().then((err, data) => {
console.log('resolve!');
console.log(err);
console.log(data);
}).catch(err => {
console.log('error!!!');
console.log(err);
});
getRestApisTest()はPromiseオブジェクトを返す。そのPromiseがどういうものかというと,AWSの通信が成功したらresolve, 失敗したらrejectになるもの。それをまず作って,成功したり失敗したりしたらいろいろ表示するのが下のコード。
Promiseの良い点は,何度も非同期処理をする場合は,thenで実行する関数でもPromiseを返せば,then, then, then...とつなげられる点。しかし,Promiseには問題がある。だるいのだ。Promiseを作るときでも,thenでも,やっぱり関数を作る必要がある。
先程述べたように,JSでは基本的に非同期スタイルで書いていくことになる。つまり,Promiseでいうと,thenがつかないところはない。だったら,関数を作りまくるんじゃなくて,昔ながらの同期プログラミングみたいに書けたほうが良いんじゃないか。そのために,async/awaitが登場した。ここから先は君の目で確認して欲しい。