TameJS と Fiber による非同期処理の記述 (2/2)

前回の続きで今回は Fiber の話題.ずいぶんと日が空いてしまいました.

見出し

  • はじめに
  • 環境
  • node-fibers とは?
  • Fiber の利用例
  • Fiber を使った非同期処理の記述例
  • 終わりに

はじめに

今回は fiber を導入し,非同期 API のコールバック周りを少し整理してみよう,ということが主題です.
node.js (というかそのエンジンである V8) は fiber をサポートしていませんが*1node-fibers を利用することで node.js 上に fiber を導入することができます.

環境

  • ubuntu 10.04 32bit (VM 上で稼働)
  • node 0.6.6
  • node-fibers 0.6.3

node-fibers とは?

node-fibers は node.js 上で fiber (coroutine) *2 をサポートするためのライブラリです.

[email protected] 以上の環境であれば,以下のように簡単に導入することができます ('Fiber' というリテラルが導入される).

$ npm install fibers

なお,Fiber の直接利用は "not recommended" であり,Future の利用を推奨しているようです (README.md の EXAMPLES 参照).
Future は並行処理ではおなじみの概念ですし,なかなか扱いやすそうです.ただ,generator のようなものは Fiber の直接利用が必要となります.

今回は実験と割り切って Fiber を直接利用しています.

Fiber の利用例

以下のコードはよくある generator です.サンプルとしても紹介しやすいですし,動作も理解しやすい使い方の一つです.

// sample.js
require('fibers');

var inc = Fiber(function() {
  var i = 0;
  while (true) {
    yield(i++);  // (1)
  }
});

var i;
while ((i = inc.run()) < 5) {  // (2)
  console.log(i);
}
$ node sample.js
0
1
2
3
4

(2) の run で Fiber が実行されます.Fiber 側で yield が呼び出されると,呼び出し元 (2) に制御が戻ります.このとき yield の引数を戻り値として返します.再度 run が呼び出されると,今度は (1) 以降の処理が再開されます.
このように処理を切り替えていくわけです.

node-fibers のコードを読んでみると,native でいろいろとやっていて (V8 との絡みとか,linux の場合 get/make/swapcontext とか) なかなか興味深いです.

Fiber を使った非同期処理の記述例

例として read input.txt → write output.txt のコードを取り上げます.
ナイーブに書くと以下の通りです.

var fs = require('fs');

var error_handler = function(e) {
  console.log(e);
};

fs.readFile('input.txt', 'UTF-8', function(err, data) {
  if (err) {
    error_handler(err);
    return;
  }
  fs.writeFile('output.txt', data, 'UTF-8', function(err) {
    if (err) {
      error_handler(err);
      return;
    } else {
      console.log('成功');
      // 処理が続く...
    }
  });
});

Fiber を使って少し工夫してみると,以下のように書くことができます.

// Fiber を再開するための汎用コールバック
var resume_cb = function(fiber) {
  return function(/*...*/) { fiber.run(Array.prototype.slice.call(arguments)); };
};

var error_handler = function(e) {
  console.log(e);
};

var main = function() {
  var fiber = Fiber.current;

  fs.readFile('input.txt', 'UTF-8', resume_cb(fiber));
  var input_res = yield();
  if (input_res[0]) {
    error_handler(input_res[0]);
    return;
  }

  fs.writeFile('output.txt', input_res[1], 'UTF-8', resume_cb(fiber));
  var output_res = yield();
  if (output_res[0]) {
    error_handler(output_res[0]);
    return;
  }

  // 処理が続く...
};

Fiber(main).run();

上記は,非同期 API を呼び出した後すぐに yield してイベントループに戻り,コールバック (resume_cb が返す function) が呼ばれたタイミングで Fiber を再開しています.
こんな使い方もできるかな,といった感じですが.

おわりに

今回は fiber を使った非同期処理の記述を取り上げました.fiber は ruby でも 1.9 から導入されていますね (最近脚光を浴びているのでしょうか?).
node.js では node-fibers を利用することで Fiber が利用可能となります.これによりコールバックネストをいくぶんか軽減することができました.

次回で node.js の control-flow あたりをまとめられたら良いですね...(できるの?)

*1:ですよね?

*2:[http://en.wikipedia.org/wiki/Fiber_(computer_science):title=fiber] は軽量スレッドと呼ばれるものです.通常のスレッドはカーネルが実行をスケジューリングしますが,fiber は明示的にスケジューリングする必要があります (協調的マルチタスクと呼ばれます.明示的に CPU リソースを明け渡さないといけないアレです.).[http://en.wikipedia.org/wiki/Coroutine:title=coroutine] は,おおざっぱに言ってしまえば処理を途中で中断/再開 (suspend/resume) できるサブルーチンです.言語レベルの機能である coroutine を実装するために fiber が利用されます.