ES.next async/await + Promise で解決できる事、とES2015(ES6) generators (yield) + Promise + npm aa (async-await) で解決できる事

  • 180
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

ES.next async/await + Promise で解決できる事

ES.next async/awaitPromise を使うと非同期処理をすごく簡単に処理できる。

とても便利なのだが、それだけでは、どうも機能が足りない様に見える。

この記事は... TL;DR
ES.next async/await を使っても、まだいろいろと課題は残ってるよ。
ES2015 (ES6) generators と npm aa (async-await) だと、より良い解決策があるよ。って話。

以下の図の様な非同期処理フローを考えてみる。

es7.png

横軸は時間だ。左から右へ時間が流れていくものと考えて欲しい。
縦線は複数の処理を同期させたい、という意味だ。

ES.next async/await 版のコード例を見てみよう。

es-next-async-await-ex.js
    'use strict';

    require('babel-polyfill'); // おまじない
    //require('regenerator').runtime();

    console.log('main: start');
    main().then(function (val) {
        console.log('main: finish:', val);
    });

    async function main() {
        var result;
        console.log('main: started');

        // シーケンシャル処理(逐次処理)
        result = await sleep(200, 'a1');
        console.log('main-a1: sleep: a1 =', result);
        result = await sleep(100, 'a2');
        console.log('main-a2: sleep: a2 =', result);
        result = await sleep(300, 'a3');
        console.log('main-a3: sleep: a3 =', result);

        // パラレル処理(並行処理)
        result = await Promise.all([sleep(200, 'b1'), sleep(100, 'b2'), sleep(300, 'b3')]);
        console.log('main-b : Promise.all([promises]): [b1, b2, b3] =', stringify(result));

        // asyncなsub()をawaitで呼ぶ
        result = await sub('c');
        console.log('main-c : sub(c) =', result);
        // asyncなsub()を並行処理で呼ぶ
        result = await Promise.all([sub('d1'), sub('d2')]);
        console.log('main-d : Promise.all([promises]): [d1, d2] =', stringify(result));

        return 'return value!';
    }

    async function sub(val) {
        await sleep(100, val + '-x1');
        console.log('sub-' + val + '-x1: sleep: ' + val + '-x1');

        await sleep(100, val + '-x2');
        console.log('sub-' + val + '-x2: sleep: ' + val + '-x2');

        return val;
    }

    function sleep(msec, val) {
        return new Promise(function (resolve, reject) {
            setTimeout(resolve, msec, val);
        });
    }

    function stringify(object) {
        var str = '';
        if (object instanceof Array) {
            for (var i = 0, n = object.length; i < n; ++i)
                str += (str ? ', ' : '') + object[i]
            return '[' + str + ']';
        }
        else {
            for (var key in object)
                str += (str ? ', ' : '') + key + ':' + object[key]
            return '{' + str + '}';
        }
    }

最初の require('babel-polyfill') は、本来のES.nextでは不要だと思うけどBabelを使う上での、おまじないだ。

await sleep(200, 'a1'); の部分は非同期処理を開始して、制御を他に譲って、
非同期処理が終わった時に戻ってくる構文だ。

  • シーケンシャルな処理であれば、await を使うだけで良い。
  • 更に深く処理を実行したければ await sub(); で良い。

やった。これでいいじゃん。と思うけど...

でも、パラレルな処理をやりたければ、Promise.all([promises,...]); などを使う必要がある。

  • 処理を2つにわけたかったら? 合流したかったら? → Promise.all を使う
  • 合流せずに途中結果だけを同期したかったら? → ???
  • 繰り返しイベントが発生する様なものは? ストリームは? インターバルタイマーは? → ???

ちょっと欲しい機能が足りないんじゃないかな。
更に、ES.nextは今はまだ標準じゃないし、もしかすると仕様が変わるかも。

ブラウザではまだ動かないし、Node.js でも動かない。Babel が必要だね。それでいいのかな。

記事書いている時にbabel 6.0が出た。plugin を使うと良い。

ES2015 (ES6) generators (yield) + Promise + npm aa (async-await) で解決できる事

ES2015 (ES6) generators (yield) と Promise + npm aa (async-await) を使っても非同期処理をすごく簡単に処理できる。

以下の図の様な非同期処理フローを考えてみる。

es6.png

ES2015 (ES6) generators (yield) 版のコード例を見てみよう。

es6-async-await-ex.js
    // require('regenerator').runtime()           // babel & browserifyするなら入れておく
    // var aa = require('aa');
    var aa = (this && this.aa) || require('aa');  // 普通は var aa = require('aa'); で良い
    var Promise = aa.Promise;                     // native Promiseがあれば不要

    console.log('main: start');
    aa(main()).then(function (val) {
        console.log('main: finish:', val);
    });

    function *main() {
        var result;
        console.log('main: started');

        // シーケンシャル処理(逐次処理)
        result = yield sleep(200, 'a1');
        console.log('main-a1: sleep: a1 =', result);
        result = yield sleep(100, 'a2');
        console.log('main-a2: sleep: a2 =', result);
        result = yield sleep(300, 'a3');
        console.log('main-a3: sleep: a3 =', result);

        // パラレル処理(並行処理) ... 配列やオブジェクトで結果を取得
        result = yield [sleep(200, 'b1'), sleep(100, 'b2'), sleep(300, 'b3')];
        console.log('main-b : parallel Array :', stringify(result));
        result = yield {x:sleep(200, 'c1'), y:sleep(100, 'c2'), z:sleep(300, 'c3')};
        console.log('main-c : parallel Object:', stringify(result));
        result = yield Promise.all([sleep(200, 'd1'), sleep(100, 'd2'), sleep(300, 'd3')]);
        console.log('main-d : Promise.all([promises,...]): [d1, d2, d3] =', stringify(result));

        // generatorのsub()をyieldで呼ぶ
        result = yield sub('e');
        console.log('main-e : sub(e) =', result);

        // パラレル処理(並行処理) ... 配列やオブジェクトで結果を取得
        // generatorのsub()を並行処理で呼ぶ ... 配列
        result = yield [sub('f1'), sub('f2')];
        console.log('main-f : [generator,...]: [f1, f2] =', stringify(result));
        // generatorのsub()を並行処理で呼ぶ ... オブジェクト
        result = yield {x:sub('g1'), y:sub('g2')};
        console.log('main-g : {x:generator, y:...}: {x:g1, y:g2} =', stringify(result));

        // 必要ないけど無理やりgeneratorのsub()をpromiseにしてみた
        result = yield Promise.all([aa(sub('h1')), aa(sub('h2'))]);
        console.log('main-h : Promise.all([aa(generator),...]): [h1, h2] =', stringify(result));

        return 'return value!';
    }

    function *sub(val) {
        yield sleep(100, val + '-x1');
        console.log('sub-' + val + '-x1: sleep: ' + val + '-x1');

        yield sleep(100, val + '-x2');
        console.log('sub-' + val + '-x2: sleep: ' + val + '-x2');

        return val;
    }

    function sleep(msec, val) {
        return new Promise(function (resolve, reject) {
            setTimeout(resolve, msec, val);
        });
    }

    function stringify(object) {
        var str = '';
        if (object instanceof Array) {
            for (var i = 0, n = object.length; i < n; ++i)
                str += (str ? ', ' : '') + object[i]
            return '[' + str + ']';
        }
        else {
            for (var key in object)
                str += (str ? ', ' : '') + key + ':' + object[key]
            return '{' + str + '}';
        }
    }

ES.next版の async function ()function *() にして、awaityield にすれば、ほぼ同じコードだ。

Node.js版は...
npm aa (async-await) を使うだけで、
ES2015 (ES6) generators (yield) はもう標準なので、そのまま動く。
※Babel を使わなくても良い

$ npm install --save aa
$ node es6-async-await-ex

HTML版の Chrome, Firefox, Edge では...
npm aa (async-await) と依存している npm promise-thunk を使うだけで、
ES2015 (ES6) generators (yield) はもう標準なので、そのまま動く。
※Babel, Browserify を使わなくても良い

es6-async-await-ex.html
<!doctype html>
<meta charset="UTF-8">
<script src="https://lightspeedworks.github.io/console-to-window/console-to-window.js"></script>
<script src="https://lightspeedworks.github.io/promise-thunk/promise-thunk.js"></script>
<script src="https://lightspeedworks.github.io/aa/aa.js"></script>
<script src="es6-async-await-ex.js"></script>

HTML版の Internet Explorer では...
Babel, Browserify を使ってください。

合流せずに途中結果だけを同期したかったら?

以下の図の様な非同期処理フローを考えてみる。

es6-fork-join.png

npm aaaa.Channel を使うと自由に同期できる。yield chan;とすると同期できる。
処理を分割(fork)して合流(join)したい時、generatorの配列を使うと良い。

es6-fork-join.js
    var aa = require('aa');
    var Channel = aa.Channel;

    console.log('main: start');
    aa(main()).then(function (val) {
        console.log('main: finish:', val);
    });

    function *main() {
        var result;
        console.log('main: started');

        var chan = Channel();

        yield [
            function *() {
                console.log('main:', yield sleep(100, 'a1'));
                console.log('main:', yield sleep(200, 'a2'));
                chan('a1-a2');  //----------------------------------+
                console.log('main:', yield sleep(300, 'a3')); //    |
            },                                                //    |
            function *() {                                    //    |
                console.log('main:', yield sleep(200, 'b1')); //    |
                console.log('main:', yield chan, '-> b'); // <------+
                console.log('main:', yield sleep(200, 'b2'));
            }
        ];

        return 'return value!';
    }

    function sleep(msec, val) {
        return new Promise(function (resolve, reject) {
            setTimeout(resolve, msec, val);
        });
    }

繰り返しイベントが発生する様なものは? ストリームは? インターバルタイマーは?

npm aaaa.Channel を使うと自由に繰り返し同期できる。

ストリームは...

es6-stream-ex.js
    var fs = require('fs');
    var aa = require('aa'), Channel = aa.Channel;

    aa(function *main() {
        var chan = Channel().stream(fs.createReadStream('es6-stream-ex.js', {encoding: 'utf8'}));
        var writer = fs.createWriteStream('es6-stream-ex.log', {encoding: 'utf8'})

        var buff;
        while (buff = yield chan)
            writer.write(buff);
        writer.end();
    });

インターバルタイマーは...

es6-interval-ex.js
    var aa = require('aa'), Channel = aa.Channel;

    aa(function *main() {
        var chan = Channel();

        var countDown = 10;
        var interval = setInterval(function () { chan(countDown--); }, 100);

        var val;
        while (val = yield chan)
            console.log('count:', val);

        clearInterval(interval);
    });

あまりにも並列度が多過ぎて過負荷になる事を避けたい時は? 並列度に制限を持たせたい。

npm executors を使うと良い。
並列処理だとすぐに終わるが、大量にリソースを食うものだと、問題になる事がある。
例えばDBアクセスとか、プロセス起動などだ。
並列度がはっきりとわかっている場合は、まだ良いが、多数のクライアントからの要求は、
並列度の制限を設けたい場合もあるのだ。

9並列になってしまう場合に3並列に抑えたいという時は、並列度に制限をかけたい所に
var executors3 = Executors(3); などとして、yield executors3(fn, args,...); とすると良い。
同時に3並列以上実行しない様にできる。

es6-executors-ex.js
    var aa = require('aa'), Channel = aa.Channel;
    var Executors = require('executors');

    aa(function *main() {
        var parallel9 = [];
        console.time('parallel9');
        for (var i = 0; i < 9; ++i)
            parallel9.push(sleep(100, i));
        yield parallel9;
        console.timeEnd('parallel9');

        var executors3 = Executors(3);
        var parallel3 = [];
        console.time('parallel3');
        for (var i = 0; i < 9; ++i)
            parallel3.push(executors3(sleep, 100, i));
        yield parallel3;
        console.timeEnd('parallel3');
    });

    function sleep(msec, val) {
        return new Promise(function (resolve, reject) {
            setTimeout(resolve, msec, val);
        });
    }

その他 promisify, thunkify など

aa.promisify(), aa.thunkify(), aa.promisifyAll(), aa.thunkifyAll() などを駆使すれば、結構いろんな事ができる。

var fs = require('fs');

// 単独のメソッドを aa で yield できる様にする。
aa.promisify(fs, 'exists', {suffix: 'A'});
// または
aa.thunkify(fs, 'exists', {suffix: 'A'});

// オブジェクトのメソッド群を aa で yield できる様にする。
aa.promisifyAll(fs, {suffix: 'A'});
// または
aa.thunkifyAll(fs, {suffix: 'A'});

if (yield fs.existsA('file.txt')) {
    var buff = yield fs.readFileA('file.txt', {encoding: 'utf8'});
}

// 例えば postgres の場合は、以下の様な感じだ。
var pg = require('pg');
aa.promisifyAll(pg.constructor.prototype);
aa.promisifyAll(pg.Client.prototype);
// etc...

参考文献

以下のリンク先も参考にして欲しい。

[JavaScript] 非同期処理のコールバック地獄から抜け出す方法 - Qiita