はじめに †今回はRuby1.9のスレッド処理を読解したいと思います。対象とするのは以下。
スレッドの作成 †thread_s_new(thread.c) †Rubyで、 Thread.new { ... } とした場合に呼ばれる処理関数はthread_s_new関数です。まずrb_thread_alloc関数(vm.c)を呼び出してrb_thread_t構造体の割り当ておよび初期化を行っています。その後、rb_boj_call_init関数経由でthread_initialize関数が呼び出されます。thread_initalize関数はエラーな状況をいろいろ処理した後、thread_create_core関数に処理を移しています。 thread_create_core(thread.c) †thread_create_core関数ではまずスレッドが実行するprocが設定されています。 rb_block_proc関数(proc.c)→proc_new関数→vm_make_proc関数(vm.c)と処理が移り、要すると渡されたブロックからProcオブジェクトを作成しています。 その後、native_thread_create関数が呼ばれプラットフォームに応じたスレッドの実装が作成されています。 vm_make_env_each(vm.c) †vm_make_proc関数が呼び出しているvm_make_env_object関数、が呼び出しているvm_make_env_each関数なのですがな〜んとなくやっていることはわかるもののいまいちどんな値の時にどんな動作をするのかが理解しづらいです。例によって具体的なケースを考えてみましょう。コードの是非はともかくサンプルはこんな感じで。 stop = false timer_th = Thread.new(0.1) {|interval| until stop print '.' $>.flush sleep interval end puts } timer_th.join(1.0) stop = true timer_th.join スタックの内容はこんな感じ。ブロックの中だと再帰的に情報を構築してるようですが面倒くさいので無視。 |stop |time_th |svar dfp,lfp→|GC_GUARDED_PTR(0) sp,bp→| ... cfp→|トップレベルのフレーム情報 トップレベルのiseqはNORMALなiseqなのでlocal_sizeは3になります。というわけでrb_env_tのメンバーは以下のようになります。 env_size = 3 + 1 + 2 local_size = 3 prev_envval = 0 また、各種ポインタは以下のように設定されます。 env->env→|stop |time_th |svar lfp,dfp→|GC_GUARDED_PTR(0) |envval # self |penvval # 0 呼び出し側のlfp, dfpも変更することでProc作成側とProcで変数が共有されるようです。 thread_start_func_1(thread_pthread.c) †ネイティブスレッドの実行関数としてthread_start_func_1関数が呼び出されます。この関数の役割はプラットフォーム独自の初期化をした上でプラットフォーム非依存のthread_start_func_2関数を呼び出し、プラットフォーム独自の終了処理を行うことなようです。 thread_start_func_2(thread.c) †というわけでスレッド実行のメインであるthread_start_func_2関数です。何やらglobal_interpreter_lockのロックを獲得していますがこのロックは、 Init_Thread(void) { ... /* acquire global interpreter lock */ rb_thread_lock_t *lp = &GET_THREAD()->vm->global_interpreter_lock; native_mutex_initialize(lp); native_mutex_lock(lp); となっているのでメインスレッドが獲得しっぱなしです。というわけで作ったスレッドは実行されません。なわけはありません。まあここら辺がどうなっているかは次のスケジューリングで解説します。 ロックを獲得した後はスレッドに設定されたブロックを実行し、実行が終了したらjoinしているスレッドを起こすなどの後始末をして処理終了です。 スレッドのスケジューリング †というわけでスレッドがどう作られて初期化されて実行されるかはわかったのですがメインスレッドがglobal_interpreter_lockを解放してくれないと作成したスレッドが実行できません。ネイティブスレッドなのでスケジューリングはしていないと思っていたのですがそうではないみたいです。RHGを見ると
ということなようです。それではいつglobal_interpreter_lockが解放されるのかを探しましょう。 RUBY_VM_CHECK_INTS(vm_core.h) †今までちらほらRUBY_VM_CHECK_INTSマクロというのを見てきたと思うのですがそろそろ中身を見てもいいでしょう。 #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()) というわけでrb_thread_execute_interrupts関数に続きます。シグナルと他から投げられた例外があったら処理した後でrb_thread_schedule関数が呼ばれています。 rb_thread_schedule(thread.c) †rb_thread_schedule関数にて、 native_mutex_unlock(&th->vm->global_interpreter_lock); { native_thread_yield(); } native_mutex_lock(&th->vm->global_interpreter_lock); となっており、global_interpreter_lockが解放されています。これで作成したスレッドの方がロックを獲得し実行が開始されています。メインスレッドは作成したスレッドが終わるまで待ちです。なわけはありません。 timer_thread_function(thread.c) †スレッドが切り替わるきっかけはいろいろありますが普通時間で切り替わります。実はInit_thread関数で Init_Thread(void) { ... rb_thread_create_timer_thread(); } thread_pthread.c rb_thread_create_timer_thread(void) { ... err = pthread_create(&timer_thread_id, &attr, thread_timer, 0); とスレッドが作られており、 thread_timer(void *dummy) { while (system_working) { struct timespec req, rem; req.tv_sec = 0; req.tv_nsec = 10 * 1000 * 1000; /* 10 ms */ nanosleep(&req, &rem); ... timer_thread_function(); } とtimer_thread_function関数が呼び出されています。ここでまたthread.cに帰ってきます。 timer_thread_function(void) { rb_vm_t *vm = GET_VM(); /* TODO: fix me for Multi-VM */ /* for time slice */ RUBY_VM_SET_TIMER_INTERRUPT(vm->running_thread); vm_core.h #define RUBY_VM_SET_TIMER_INTERRUPT(th) ((th)->interrupt_flag |= 0x01) ということで現在実行しているスレッドに割り込みフラグが設定され次のRUBY_VM_CHECK_INTSのタイミングでスレッドが切り替わります。 おわりに †今回はRuby1.9のスレッド処理を見てきました。Ruby1.9ではネイティブスレッドが使われるようになったのですがスレッドは好き勝手に実行されるというわけではなくロックによってどのスレッドが実行されているかということが管理されています。 ではネイティブスレッドにしても意味ないんじゃないのか?という疑問についてですがRHGでは、
と書かれているものがRuby1.9ではわずか18行です。これだけでも十分ネイティブスレッドにする意味があると思います。コードを書かなければ書かないほどバグは作られないですから*1。 それではみなさんもよいコードリーディングを。 |