Unless you’ve been living under a rock for the past few years you will have heard of C-ruby’s GIL/GVL, the Global Interpreter/VM Lock (I prefer to think of it as the Giant VM Lock). While your ruby threads may sit on top of native pthreads, this big bad lock stops more than one thread running at the same time. Allegedly one of the main reasons behind this was to protect non threadsafe ruby extensions and also to shield us from the horrors of threading. Personally it feels like a lot of 3rd party gems needed updating for 1.9 anyway (particularly with encoding related issues) and so it would have been a good opportunity to make that change. The complexities of threading, locking etc. could be handled by providing higher level abstractions over them (actors etc.).
True concurrency isn’t completely dead though. Ruby can already release the GVL when a thread is blocked on IO and if you are writing a C extension you can release the lock too. The mysql2 gem does this: clearly there is no point in holding onto the GVL when you’re just waiting on mysql to return results. Similarly Eric Hodel recently submitted a patch to the zlib extension so that the lock is released while zlib is doing its thing. This obviously doesn’t make mysql queries or zlib run any faster individually but it means you can run many in parallel and that these operations don’t block other unrelated threads. When even laptops have hyperthreaded quad-core processors, this is a good thing.
The magic API is
rb_thread_blocking_region, whose header documentation comes with copious warnings. Threading after all is hard (and should not be mixed with alcohol (I speak from experience)).
A call to
rb_thread_blocking_region looks like this
rb_thread_blocking_region(do_some_work, argument, unblocker, unblocker_argument);
When you call this ruby
- releases the GVL (other ruby threads can now run)
- calls your
do_some_workfunction , passing it
- reacquires the GVL
Operating without the GVL is a scary place to be. You can’t in general call any of the C-ruby api, because they all assume they hold the GVL. If you’ve ever written MP threaded code on Apple’s OS 8 you’ll feel right at home.
The second pair of arguments is the so called unblocking function (ubf) and its argument. If ruby needs to kill your thread (in response to
Thread.kill, the VM exiting, etc) this function will be called. Your
do_some_work should then exit. For example ruby’s
bignum.c has this code that runs inside
1 2 3 4 5 6 7 8 9
The ubf just sets the
bds->stop flag so that
bigdivrem1 returns early.
You can specify the constants
RUBY_UBF_IO to use the ruby provided
ubf_select function which handles the common case of being blocked on a call to
accept or other such functions (it’s not a general purpose ubf – see posting to ruby-core. The overall intent is that you don’t do an awful lot inside your ubf, just enough to get your main
do_some_work function to stop. Sometimes there just isn’t a good way to stop what you’re doing; it’s not the end of the world if your ubf does nothing.
This works best when you can isolate a chunk of C code that doesn’t need any interaction with the ruby world. Code that wants to frequently call back into ruby (for example a sax style xml parser delivering events to a ruby class) isn’t a good fit.
In some cases you might consider
rb_thread_call_with_gvl, which reacquires the GVL, executes some code for you and releases it. The headers around it are plastered with warnings about it being experimental, might be removed in ruby 1.9.2 but it would seem that it is here to stay. If you end up calling it a lot then you’re pretty much back to executing serially.
A simple example
The rdiscount gem (a markdown parser) presents a reasonable opportunity for this sort of work. RDiscount objects have a
to_html method doesn’t do much other than call into the discount c library. Initially the core of this method looked like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
It just pushes a string through the markdown library and creates a ruby string from the result. The first thing I did was to write a small benchmark that loads a few hundred markdown file and converts them to html. I pulled the IO part out of the benchmark part because that wasn’t the bit I was interested in.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
With vanilla rdiscount/master, this produces the following output on my quad-core iMac
1 2 3 4
This is pretty fast – the folder contained nearly 1400 files that we parsed 40 times over and it still only took a handful of seconds. However the results are identical no matter how many threads (to within 0.5%). The cpu usage (as measured by top) never goes above 100% (On OS X 100% means 100% of one core, so values of 400% on a 4 core machine are possible).
My changed version of
rdiscount.c looks like this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
The setup code now marshals all of the parameters needed into a struct of type
rb_discount_to_html_param_block and then uses
rb_thread_blocking_region to execute
rb_rdiscount_to_html_no_gvl which does the bulk of the work.
This time the output looks like this, on the same quad-core iMac.
1 2 3 4
The 2 thread case now runs in 55% of the time it took to run the 1 thread case. and the 4 thread case runs in 34% of the time. The most we could hope for was to halve execution time with 2 threads and quarter it for 4 threads, so we got pretty close. CPU utilisation is also much higher. There is of course some code which for which the GVL is held and there’s always some overhead. Still, not bad for what was essentially a 5 minute change!
Cleaning up after yourself
This code has one flaw: should ruby try to kill the thread then
rb_thread_blocking_region won’t ever return. When the GVL is reacquired ruby will check whether the thread should be killed and bail out if appropriate. In our case we would leak the resources allocated by
mkd_string. One way around this is to ensure anything that is allocated by
rb_discount_no_gvl is also disposed of insode
rb_discount_no_gvl. That doesn’t really work for in this case: we need to be able to convert the result back into a ruby object, and we can’t do that in the no-man’s land that is
rb_thread_call_with_gvl doesn’t help since it also checks whether the thread should be killed when the GVL is reacquired.
In pure ruby when you want to make sure code gets executed even in the presence of such things you use
ensure, and things are not so different (if more verbose) when using the C api: the
rb_ensure function allows you to call a function while specifying a second function that should be called after, no matter what.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
This ensures that the cleanup/conversion code always run. Properly cleaning up after yourself in this sort of situation is definitely tricky.
When it’s a good fit,
rb_thread_blocking_function can be pretty handy but it’s definitely a little verbose (if consistent with the rest of the ruby API). Perhaps exposing the internal
BLOCKING_REGION macro would help ease some of the callback spaghetti. A better way of dealing with libraries that want to callback into ruby would be great too.