Boost.Context について調べた

この記事は Boost Advent Calendar 13 日目の記事です。


Boost.Context は、跋扈するコンテキスト切り替えの利用を統一するべく颯爽と現れた救世主です(大げさ)。


そもそもコンテキスト切り替えって何ぞや?という場合は、適当にググるか、自分の Boost.Coroutine の発表資料あたりを読んで下さい。


現在コンテキスト切り替えを使ったライブラリで提案されているのは、LibrariesUnderConstruction – Boost C++ Libraries を調べた感じだと、

あたりのようです。
これらは Windows の Fiber や UNIX 系の ucontext 系の関数、あるいはアセンブラを直接利用してコンテキスト切り替えを行うベース部分があり、その上にライブラリを構築しているのですが、このベース部分だけをライブラリに切り出してやろうというのが、Boost.Context の目標です。


ということで Boost.Context は「ライブラリ制作者のためのライブラリ」であるためユーザがこれを利用する必要は全く無いのですが、個人的に面白そうなので触ってみたいなーとか思っていて、丁度いい具合に Boost Advent Calendar がやってきたので、登録ドリブン開発(RDD:Register Driven Development)で調べてみることにしました。

ソース

ソースはさっきと同じところに置いてあります。

Boost.Context は、バージョン 1.47 の時点ではまだ採用されていない Boost.Move に依存しているので、Boost.Move も一緒に入ってます。
が、ここにあるのはかなり古いのでやめておきましょう。
最新版は、ググった感じだと

ここにあるようなので、こちらを使いましょう。

ドキュメント

ドキュメントはここにあります。

ドキュメントは結構丁寧に書かれていて、これ読めば使い方は分かりそうです。
ということで、わざわざビルドするのも面倒ですし、ドキュメントを見て空想で書くことにします。
(つまり以下のコードは全て妄想です)

boost::contexts::context

context クラスは、コンテキスト切り替えの高レベルなラッパーです。
こんな感じで使います。

context ctx;

// コンテキスト関数。この中で suspend をすると、処理を中断して呼び出し元に処理を返すことができる。
// 再度 resume が呼び出されたときは、suspend した位置から復帰することができる。
void f(int n) {
  for (int i = 0; i < n; i++) {
    std::cout << i << ", ";
    ctx.suspend();
  }
}

int main() {
  ctx = context(f, 42, default_stacksize(), no_stack_unwind, return_to_caller);
  ctx.start(); // ここで f(42) が実行され、0, が出力され、ctx.suspend によってこちらに復帰する
  while (!ctx.is_completed()) // f 関数の実行が全て終わったら is_completed が true になる
    ctx.resume(); // ここで 1, 2, 3, ... 41, と順番に出力されていく
}

f と 42 の部分は、std::thread と同じように、関数と引数を渡します。引数が2つある場合は2つ渡すだけです(つまり Variadic Template みたいなもの)。
default_stacksize() の部分を弄ることでスタックサイズを変更できるようです。

unwind

no_stack_unwind は、スタックを破棄しない、つまりコンテキスト関数のローカル変数のデストラクタが呼ばれないということを意味します。

void f(context& ctx) {
  Hoge hoge;
  while (true)
    ctx.suspend();
}

int main() {
  context ctx;
  ctx = context(f, std::ref(ctx), default_stacksize(), no_stack_unwind, return_to_caller);
  ctx.start();
} // 危険! hoge オブジェクトのデストラクタが呼び出されない

この場合、context::unwind_stack() 関数を呼び出して明示的に破棄するか、

int main() {
  context ctx;
  ctx = context(f, std::ref(ctx), default_stacksize(), no_stack_unwind, return_to_caller);
  ctx.start();
  // ここで hoge のデストラクタが呼び出されて f 関数の実行は終了する
  ctx.unwind_stack();
}

あるいは stack_unwind を指定してやると、ctx の破棄時にデストラクタを呼んでくれるようです。

int main() {
  context ctx;
  ctx = context(f, std::ref(ctx), default_stacksize(), stack_unwind, return_to_caller);
  ctx.start();
} // ここで hoge のデストラクタが呼び出されて f 関数の実行は終了する

もし f 関数の実行を途中で終わらせる可能性があるなら、stack_unwind を指定しておいた方がいいでしょう。

return_to_caller

あと context コンストラクタの最後に渡している return_to_caller ですが、これは「f 関数の実行が終わったらちゃんと start や resume 関数を呼び出した元へ戻ってくる」ということを意味します。
もし exit_application を指定した場合、コンテキスト関数が終了すると同時にアプリケーションも終了するそうです。
これは多分、while (true) なんかで回って絶対に処理を戻さないパターンや、main を踏み台として全ての処理をコンテキスト関数で行う場合なんかを考慮しているのでしょう。多分。
で、さらにこの部分には別の context オブジェクトを入れることができます。
そうした場合、コンテキスト関数を終了したとき、その context オブジェクトが実行されることになります。

context ctx1;
context ctx2;

void f() {
  for (int i = 0; i < 5; i++) {
    std::cout << "f(" << i << ")" << std::endl;
    ctx1.suspend();
  }
}

void g() {
  for (int i = 0; i < 5; i++) {
    std::cout << "g(" << i << ")" << std::endl;
    ctx2.suspend();
  }
}

int main() {
  ctx2 = context(g, default_stacksize(), no_stack_unwind, return_to_caller);
  ctx1 = context(f, default_stacksize(), no_stack_unwind, ctx2);

  ctx1.start();
  while (!ctx1.is_completed())
    ctx1.resume();

  // f 関数の終了時に ctx2.start() が呼ばれているので呼び出す必要はない
  // ctx2.start();
  while (!ctx2.is_completed())
    ctx2.resume();

}
exception

コンテキスト関数の中で例外が投げられた場合、std::terminate() が呼ばれます。

stack_allocator

context クラスのコンストラクタの最後の引数に、スタック用のアロケータを指定することができます。
これは stack_allocator コンセプトを満たしているアロケータを指定することになります。
といっても単に allocate 関数と deallocate 関数を実装するだけなので簡単です。
ただし、スタックオーバーフロー検出のために、ガードページ等を入れておくことが強く推奨されます。

引数と戻り値

start, resume, suspend 時にデータのやりとりを行うために、それぞれの関数に void* な引数や戻り値が用意されています。
start() や resume() 呼び出しの戻り値は suspend(p) 呼び出しの引数 p の値になり、suspend() 呼び出しの戻り値は resume(p) 呼び出しの引数 p の値になります。
これを使えば、簡易的な yield 処理が実現できます。

context ctx;

void f(int v) {
  int x = 1;
  int y = 1;
  int r;

  // 与えられた値を、n 項のフィボナッチ数を掛けて返す
  r = v * x;
  v = *static_cast<int*>(ctx.suspend(&r));
  r = v * y;
  v = *static_cast<int*>(ctx.suspend(&r));
  while (true) {
    int z = x + y;
    r = v * z;
    v = *static_cast<int*>(ctx.suspend(&r));
    x = y;
    y = z;
  }
}

int main() {
  int v;
  ctx = context(f, 10, stack_unwind, return_to_caller);
  std::cout << *static_cast<int*>(ctx.start())    << std::endl; // 10 * 1
  v = 20;
  std::cout << *static_cast<int*>(ctx.resume(&v)) << std::endl; // 20 * 1
  v = 30;
  std::cout << *static_cast<int*>(ctx.resume(&v)) << std::endl; // 30 * 2
  v = 40;
  std::cout << *static_cast<int*>(ctx.resume(&v)) << std::endl; // 40 * 3
  v = 50;
  std::cout << *static_cast<int*>(ctx.resume(&v)) << std::endl; // 50 * 5
  v = 60;
  std::cout << *static_cast<int*>(ctx.resume(&v)) << std::endl; // 60 * 8
}

キャストがひどいことになってますが、これは Boost.Context の上に作るライブラリがうまく型をいい感じに処理してくれるライブラリを作ってくれるはずなので、ここはこれで構わないでしょう。

boost_fcontext_t

boost_fcontext_t は、コンテキスト切り替えの低レベル部分のラッパーです。
これは C 言語からでも使うことができるように、グローバル名前空間の中に extern "C" で宣言されています。
ucontext_t の実装に似せているそうなので、使ったことのある人にはなじみがあるんでしょう、多分。
詳しくはドキュメント見ましょう。

まとめ

ドキュメントを読む限り、レビューで指摘されていたことも結構直されているし、Boost.Move も 1.48.0 で入るし、そろそろ Boost.Context も Boost の仲間入りしそうな感じです。
そうなると Boost.Context を利用した高度なライブラリなんかも入ってきて、他の言語でうらやましかった yield 処理なんかも簡単に書けるようになってかなりいい感じになるんじゃないかなーと思います。