Rubyのスレッドはタイムスライスで切り替わる?

Techc, Japanese, Ruby

(English Version)

Rubyのスレッド切り替えのトリガーは?

 
昨日参加したkawasaki.rbで、スレッドの実行の切り替わりのタイミングがよく分からない、という話題がでました。

Ruby1.9でネイティブスレッドを用いた方式になったけれど、GVL(Giant VM Lock)の仕組みでIO以外の処理では同時に実行されるスレッドは1一つに制御されている、というような説明をネットで見かけました。
もしIO処理やスリープのタイミングでしかスレッドが切り替わらないとすると、下記のようなスレッド内でビジーループな処理をしているコードを動かすと・・・

for item in %w(first second third)
    puts item
    Thread.fork item do |param|
        (1..10000).each { |a| a ** a }
        puts "thread output:"+param
    end
end
(Thread.list - [Thread.current]).each &:join

(期待する出力)

first
second
third
thread output:first
thread output:third
thread output:second

3つのthread output:****の表示が順番にゆっくり表示されるのでは?と思われます。
(一つ目のスレッドの”(1..10000).each { |a| a ** a }”が完了したら、putsして、次のスレッドの処理に移る、を繰り返す)

しかし実際は、3つの”thread output”の表示は3つ連続して一瞬で表示されます。
どうもIO処理以外にもスレッドの処理が切り替わるタイミングがあるようです。

Rubyのスレッド切り替えにはタイマーが利用されている

少し調べてみると、一定時間ごとにスレッドを切り替える仕組みもあるようです。
例えば↓のささださんの資料。
https://atdot.net/~ko1/activities/prosym2011-thread_kaizen_slide.pdf

資料は2011年のもので、最新のRubyの状況が分からなかったので、最新のRubyのソースコード(ruby 2.6.0dev (2018-04-26 trunk 63262) )を取ってきて調べてみました。

調べてみると、昔はeval.cの中にスレッドスケジューリング関連の実装があったようですが、現在は、thread.cあたりに実装されているようです。
で、thread_pthread.cに実際にタイマースレッドが存在することを見て取れます。

static void *
thread_timer(void *p)
{
    rb_global_vm_lock_t *gvl = (rb_global_vm_lock_t *)p;

・・・中略・・・

    while (system_working > 0) {

    /* timer function */
    ubf_wakeup_all_threads();
    timer_thread_function(0);

    if (TT_DEBUG) WRITE_CONST(2, "tick\n");

        /* wait */
    timer_thread_sleep(gvl);
    }

ここがタイマースレッドの本体と思われます。
timer_thread_function() 内部でスレッド切り替えの判定を行っているようです。


static void
timer_thread_function(void *arg)
{
    rb_vm_t *vm = GET_VM(); /* TODO: fix me for Multi-VM */

    /*
     * Tricky: thread_destruct_lock doesn't close a race against
     * vm->running_thread switch. however it guarantees th->running_thread
     * point to valid pointer or NULL.
     */
    rb_native_mutex_lock(&vm->thread_destruct_lock);
    /* for time slice */
    if (vm->running_thread) {
    RUBY_VM_SET_TIMER_INTERRUPT(vm->running_thread->ec);
    }
    rb_native_mutex_unlock(&vm->thread_destruct_lock);

    /* check signal */
    rb_threadptr_check_signal(vm->main_thread);

“RUBY_VM_SET_TIMER_INTERRUPT(vm->running_thread->ec);” で現在実行中のスレッドにフラグを立てて、処理の切り替えのトリガー設定をしてるようです。

タイマースレッドを無効にすると?

 
では、タイマースレッドを無効にしたらどうなるのか?
ということで、thread_timer() 内部の while ループをただsleepするだけのループに書き換えて、Rubyをビルドし直してみると・・・


first
second
third
※少し間が空く
thread output:first
※少し間が空く
thread output:second
※少し間が空く
thread output:third
※プロセスが終了しない

当初期待した出力になりました!
プロセスが終了しなくなってしまったので、joinの処理にも影響与えているようです。

結論

 
少なくともruby 2.6.0devでは、タイマースレッド(ネイティブ)によるRubyスレッドの切り替えが実装されている。

補足

IO処理中のスレッドの動き

IO処理中のスレッドの切り替わりについては、↓の記事が参考になりました。Rubyのスレッド内の処理は常にスレッドセーフ!と思っていると危ないですね。

Rubyのビルドの工夫

Rubyをビルドするとき、タイマーを無効にすると、minirubyにも影響してしまって、ビルドが終わらなくなってしまいます。
実験するときは、起動時の引数でタイマースレッド内部の処理を切り替えるようなことを行いました。

ビジーループ

最後の確認では、きれいに出力するには、スレッドの起動が終わる前にビジーループ処理が始まらないように調整必要です。そうしないと、”puts item”のタイミングで、一つ目のスレッドの処理が割って入ってきて、”puts item”が遅延されてしまいます。


for item in %w(first second third)
    puts item
    Thread.fork item do |param|
        sleep(0.1) #3スレッドの起動が終わるまでの猶予
        (1..10000).each { |a| a ** a }
        puts "thread output:"+param
    end
end
(Thread.list - [Thread.current]).each &:join

Techc, Japanese, Ruby

Posted by kishima