How is Ruby’s thread scheduling system implemented?

Techc, English, Ruby

(Japanese version)

What is the trigger of switching Ruby threads?

 
Last night, I attended kawasaki.rb. An attendee said “When does Ruby swtich threads?”.

I found some explanations which say that Ruby after 1.9 uses native threads, however it runs one thread only based on GVL(Giant VM Lock) system except for IO blocking condition.
If this statement is true, it means a context-switching never happens in a busy loop method like,

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

(Expected result)

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

Three lines like “thread output:” would be shown one by one slowlly, it’s expcted.
But the three lines are shown in very short time.
It seems there is another condition which produces a context-swithcing.

Time slicing in Ruby

According to the following document, Ruby supports a time slicing by a timer thread.

クリックしてprosym2011-thread_kaizen_slide.pdfにアクセス


(Document from Sasada who is a core Ruby commiter)

This document is slightly old, so I’ve downloaded latest source code (ruby 2.6.0dev (2018-04-26 trunk 63262) ) and checked it.

I found a timer thread implementation in thread_pthread.c (It was impleneted in eval.c in old Ruby version).

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);
    }

It look this point is the main body of the timer thread and timer_thread_function() does something about switching threads.


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)” sets a flag of the running thread which indicates the thread needs to be switched, and then the VM will switch the context.

What will happen withoud the timer?

OK, let’s see. I replaced the while loop in thread_timer() with an empty loop and rebuilded the Ruby.


first
second
third
(no output for a moment)
thread output:first
(no output for a moment)
thread output:second
(no output for a moment)
thread output:third
(the script never ends)

This is an expected result!
It seems the timer affects “join” implementation as well.

Conclusion

Latest Ruby(ruby 2.6.0dev) also supports time slicing by a timer thread(native thread).

Supplements

A problem about building Ruby

If you disable the timer thread, it will also affect miniruby. The build process will stuck in miniruby finally.
I implementeded a global flag to select behavior of the timer thread.

Busy loop behavior

In order to get exptected results, I added “sleep(0,1)” in top of a thread. Since a thread can interrupt main thread in “puts” which is also IO.


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

Techc, English, Ruby

Posted by kishima