ひとり勉強会

ひとり楽しく勉強会

スレッド切り替え

1個だけ実行するスレッドを、グローバルなMutexのロックという形で明示的に管理しているので、スレッドの切り替えの部分でもロックの取得/解放を明示的に実装することになります。これをやるのが rb_thread_schedule @ thread.c 関数。

void
rb_thread_schedule(void)
{
  if (!rb_thread_alone()) {
    rb_thread_t *th = GET_THREAD();
    rb_gc_save_machine_context(th);

    native_mutex_unlock(&th->vm->global_interpreter_lock);
    {
      native_thread_yield();
    }
    native_mutex_lock(&th->vm->global_interpreter_lock);

    rb_thread_set_current(th);
  }
}

色々省略しつつコードを引用しています。概要はこんな感じ。複数スレッドが走っている時にrb_thread_scheduleを呼び出すと、グローバルロックを解放して、他のスレッドに処理を譲り(native_thread_yieldなので、次にどのスレッドが実行開始されるかはOSのスケジューラが決めます)ます。しばらくするとこのスレッドに処理が戻ってくるので、そのタイミングでグローバルロックを獲得して、自分自身をカレントスレッドと宣言してから、rb_thread_schedule を抜けて続きの処理に進みます。
rb_thread_schedule は割り込みを実行する rb_thread_execute_interrupts @ thread.c 関数の中で勝手に呼び出される実装になっていて

void
rb_thread_execute_interrupts(rb_thread_t *th)
{
  while (th->interrupt_flag) {
    ...
    rb_thread_schedule();
  }
}

rb_thread_execute_interrupts は RUBY_VM_CHECK_INTS @ yarvcore.h マクロによって実行されます。

#define RUBY_VM_CHECK_INTS_TH(th) do { \
  if(th->interrupt_flag){ \
    /* TODO: trap something event */ \
    rb_thread_execute_interrupts(th); \
  } \
} while (0)

#define RUBY_VM_CHECK_INTS() \
  RUBY_VM_CHECK_INTS_TH(GET_THREAD())

なので、YARVVM実装では、割り込みチェックとスレッド切り替えを行いたいタイミングで RUBY_VM_CHECK_INTS() を実行するようになっています。insns.def を見たところ、現在は

  • send
  • leave
  • jump
  • branchif
  • branchunless

の5つの命令の実行時に RUBY_VM_CHECK_INTS() しているようでした。たとえばこんな

DEFINE_INSN
branchif
(OFFSET dst)
(VALUE val)
()
{
  if (RTEST(val)) {
    RUBY_VM_CHECK_INTS();
    JUMP(dst);
  }
}

感じです。要するに、ジャンプ命令のところでチェックしているわけですね。ジャンプなしで無限ループすることはできないので、これで、ある程度の間隔で必ずスレッド切り替えが行えるようになっているようです。opt_plusとSystemStackErrorとensureで疑似無限ループしてみるとどうだろう…と思って試してもうまく切り替わったので、他にも切り替え箇所があるのかな?