スレッドセーフなmruby-signal-threadを書いた

最近仕事でmrubyを触り始めて、だいぶC言語に触れ合う時間が増えてきたので、@matsumotoryのススメもあって、mgemを書きました。

なぜ作ったのか

mrubyには既にmruby-signalというCRubyのクローン実装のmgemも存在するのですが、マルチスレッドでの動作を考えた場合に、グローバル変数にmrb_stateを持つことから、意図しない動作となるケースがあるため再実装しました。シングルプロセス、シングルスレッドで使うにはmruby-signalで十分なので、今回はコントリビュートではなく別実装とした意図があります。

使い方

シンプルにこのような実装にしました。mruby-threadにシグナルを指定可能にしたようなイメージが近いと思います。

SignalThread.trap(:HUP) do
  puts "foo"
end

puts "wait..."
loop { sleep 1 }
$ mruby/bin/mruby example/signal_thread.rb &
wait...
$ kill -HUP $(pidof mruby)
foo

工夫

実装するにあたり工夫が必要だったのは、下記のコードです。

static mrb_value mrb_signal_thread_wait(mrb_state *mrb, mrb_value self)
{
  int sig, s;
  mrb_value *argv;
  mrb_int argc;
  sigset_t set, mask;
  mrb_value command, block;

  mrb_get_args(mrb, "*&", &argv, &argc, &block);

  if (!mrb_nil_p(block) && MRB_PROC_CFUNC_P(mrb_proc_ptr(block))) {
    mrb_raise(mrb, E_RUNTIME_ERROR, "require defined block");
  }

  sig = trap_signm(mrb, argv[0]);

  // 全てのシグナルをセットする
  sigfillset(&mask);
  // 処理したいシグナルだけを削除する
  sigdelset(&mask, sig);
  if (pthread_sigmask(SIG_BLOCK, &mask, NULL) != 0) {
    mrb_raise(mrb, E_RUNTIME_ERROR, "set mask error");
  }

  sigemptyset(&set);
  sigaddset(&set, sig);

  for (;;) {
    sigwait(&set, &sig);
    mrb_yield_argv(mrb, block, 0, NULL);
  }
}

今回の実装はSignalThread#trapが実行されるごとに、mruby-threadを利用して、そのシグナルを処理する専用のスレッドを起動し、スレッドでsigwaitするような実装としています。しかし、その場合、複数のスレッドを起動した場合に、先に起動したスレッドに後続のスレッドが処理するはずのシグナルが配送されてしまう事が起こりえます。

  1. HUPを処理するスレッド1を起動
  2. USR1を処理するスレッド2を起動
  3. USR1シグナルを送る

この場合に、スレッド1にUSR1が配送されてしまうケースに該当します。スレッド1のsigwaitはHUPのみを待ち受けているのですが、そこにUSR1が配送されても意図したハンドラ処理を行うことが出来ません。この辺のシグナルの流れは同僚の@harasouが書いたLinux シグナルの基礎が詳しいです。

この問題を避けるために、SignalThread#trapから起動するスレッド内で実行されるSignalThread#waitでは、引数に指定されたシグナル以外は全てpthread_sigmaskでマスクして、sigwaitすることで、意図しないシグナルの配送を防ぐ実装にしています。

最後に

これまでmgemコントリビュート中心に関わっていた、mrubyにおいて初めて自分でmgemを実装したわけですが、mruby開発の面白さとして、Rubyの実装をC言語で書いて、さらには今回のようにmrblibで既存のgemと組み合わせて一つのバイナリとするような、これまでとは違った融合的な楽しさがあるなと感じています。開発者としてほんとにチャンスが多い言語だと思うので、これからさらに食い気味に関わっていきたいと思います。
そして来年は100万円狙っていく。

100万円の漢