3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

非同期処理と同期処理が混ざった場合の実行結果

Posted at

参考文献

記事の背景

私は javascript と Node.js の初学者で、非同期処理と同期処理の概念的なイメージはつかんできたものの、それを踏まえて実際に適切なプログラムを作れるかと言われると不安がある。なので理解を深めるために、とあるケースの挙動を追ってみた。

CPS(the continuation-passing style)

In JavaScript, a callback is a function that is passed an argument to another function and is invoked with the result when the operation completes. In functional programming, this way of propagating the result is called continuation-passing style, for brevity, CPS. It's a general concept, and it is not always associated with asynchronous operations.

関数型プログラミングの世界では、コールバックを引数にとり、ある処理の結果にそれを適用するやりかたをCPSと呼ぶらしい。また、このスタイルは、必ずしも非同期処理と関連付けられているわけではない。

なるほど。コールバックを使うスタイルでも、同期的に処理することがあるということかな。ちなみにコールバックを使わずに同期的に処理する、いわゆる普通のスタイルは、direct styleと呼ぶそうだ。

ということで今回のケースとは、同期的CPS, 非同期的CPSを混ぜて使ったらどのような挙動を示すかというものになる。

サンプルコード

test.js
var fs = require('fs');
var cache = {};

function inconsistRead(filename, callback) {
    if (cache[filename]) {
        //invoked synchronously
        callback(cache[filename]);
    } else {
        //asynchronous function
        fs.readFile(filename, 'utf8', function(err, data) {
            cache[filename] = data;
            callback(data);
        });
    }
}

function createFileReader(filename) {
    var listeners = [];
    var cnt = 0;
    inconsistRead(filename, function(value) {
        listeners.forEach(function(listener) {
            console.log(++cnt);
            listener(value);
        });
    });
    return {
        onDataReady: function(listener) {
            listeners.push(listener);
        }
    };
}

var reader1 = createFileReader('data.txt');
reader1.onDataReady(function(data) {
    console.log('First call data : ' + data);

    // reader1を作成した後のタイミングでreader2を作成したとする。
    // この時reader2は同期的に作成されるため、`listeners`は空リストのまま。
    // よって作成時のコールバック処理は発生しない。
    reader2 = createFileReader('data.txt');
    reader2.onDataReady(function(data) {
        // このブロック内の処理はリスナとして登録されるものの、呼び出されることなくお蔵入りとなる。
        console.log('Second calll data : ' + data);
        
        // したがって、このコードが実行されることはない。
        reader3 = createFileReader('data.txt');
        reader3.onDataReady(function(data) {
            console.log('Third call data' + data);
        });
    });
});

// 一方、reader4が作成される時点で`cache`はまだ空なので、
// この処理は非同期的に行われる。
reader4 = createFileReader('data.txt');

// だから、登録したリスナは、期待通りに処理される。
reader4.onDataReady(function(data) {
    console.log('Forth call data : ' + data);
});
reader4.onDataReady(function(data) {
    console.log('Forth call, second listener.');
});

実行結果

直感的に期待した結果にはならない。また、トップレベルで作成したreader1reader4に対するコールバック処理の順序は、必ずしもトリガーを引いた順にはならない点にも注意(下記はたまたま、そのようになっているだけ)。

実行結果
$ node test.js
1
First call data : testvalue


1
Forth call data : testvalue


2
Forth call, second listener.

終わりに

参考文献のサンプルコードを改変して試してみたら、いろいろと理解が深まった。特に、1回しか呼び出されないような処理をコールバックとして渡す場合は、その渡し方で本当に処理されるのかどうかを見極める必要がある。
今回のケースでは、コールバック処理を一旦リスナとして登録しオブジェクト生成時に一度だけ呼び出すような形をとったものの、生成時にそのリスナが登録されていない事態が生じている。

なおこのような混乱を防ぐためとして、参考文献に大きく2つの指針があった。

  1. ひとつの関数に、直和的に同期処理と非同期処理をまぜない
  2. 同期処理を選択したなら、direct style でプログラムする
3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?