17

Ruby 1.9 is supposed to have native threads, and GIL is supposed to lift if some threads enters native code (like GUI toolkit main loop or C implementation of some Ruby lib).

But if i start following simple code sample that displays GUI in main thread and do some basic math in separate thread - the GUI will hang out badly, try to resize window to see it yourself. I have checked with different GUI toolkit, Qt (qtbindings gem) - it behaves exactly same. Tested with Ruby 1.9.3-p0 on Windows 7 and OSX 10.7

require 'tk'
require 'thread'
Thread.new { loop { a = 1 } }
TkRoot.new.mainloop()

Same code in Python works fine without any GUI hangs:

from Tkinter import *
from threading import *
class WorkThread( Thread ) :
  def run( self ) :
    while True :
      a = 1
WorkThread().start()
Tk().mainloop()

What i'm doing wrong?

UPDATE

It seems where is no such problem on Ubuntu linux, so my question is mainly about Windows and OSX.

UPDATE

Some people points out that where is no such problem on OSX. So i assembled out a step-by-step guide to isolate and reproduce a problem:

  1. Install OSX 10.7 Lion via "Recovery" function. I used our test department MB139RS/A mac mini for test.
  2. Install all updates. The system will look like this: enter image description here
  3. Install latest ActiveTcl from activestate.com, in my case it's ActiveTcl 8.5.11 for OSX.
  4. Download and unpack latest Ruby source code. In my case it's Ruby 1.9.3-p125. Compile it and install replacing system Ruby (commands below). You will end up with latest ruby with built-in Tk support: enter image description here
  5. Create a test.rb file with code from my example and run it. Try resizing a window - you will see terrible lags. Remove thread from code, start and try resizing a window - lags are gone. I recorded a video of this test.

Ruby compilation commands:

./configure --with-arch=x86_64,i386 --enable-pthread --enable-shared --with-gcc=clang --prefix=/usr
make
sudo make install
Coren
  • 5,517
  • 1
  • 21
  • 34
grigoryvp
  • 40,413
  • 64
  • 174
  • 277
  • Not sure, but has Ruby 1.9 still got a GIL (Global Interpreter Lock)? That'd totally explain your problem... – Romain Jan 30 '12 at 12:40
  • @Romain How GIL explains my problem? Python has same GIL and no problem. – grigoryvp Jan 30 '12 at 12:52
  • GIL means only a single thread can run ruby code at once, so if you background calculation can use it, your UI code cannot. – Romain Jan 30 '12 at 12:58
  • 1
    @Romain Python has same GIL and no such problems. Ruby scheduler will stop background thread after some time (around 100 ruby instructions?) and give some CPU time to another threads. Doing such switches very fast, Ruby will achieve near parallel execution. For example, if you start two ruby threads that will both run ruby code, Ruby will switch between threads very fast, so for you they will be executed just like in parallel. – grigoryvp Jan 30 '12 at 13:01
  • Makes sense. Don't see what's wrong with your code anyhow. And since it cannot be the GIL... – Romain Jan 30 '12 at 13:12
  • Somehow it does not seem to be an issue on (Arch)Linux. =/ – Mereghost Jan 30 '12 at 19:31
  • @Mereghost Try to resize window instead of moving it - some OS'es will draw window movement smoothly event if windows is not responding. – grigoryvp Jan 30 '12 at 21:21

4 Answers4

11

This hang can be caused by C code of Ruby bindings in Toolkit. As you know, ruby threads have a global lock : the GIL. It seems that mixing between Ruby bindings' C thread, Tk C thread and Pure Ruby thread is not going well.

There's a documented workaround for a similar case, you can try to add those lines before require 'tk' :

module TkCore 
  RUN_EVENTLOOP_ON_MAIN_THREAD = true
end

Graphical toolkit needs a main thread in order to refresh graphical elements. If your thread is in an intensive computation, your thread is requesting heavily the lock and so it is interfering with toolkit's thread.

You can avoid use of sleep trick if you want. In Ruby 1.9, you can use Fiber, Revactor or EventMachine. According to oldmoe, Fibers seems to be quite fast.

You can also keep Ruby threads if you can use IO.pipe. That's how parallel tests were implemented in ruby 1.9.3. It seems to be a good way to workaround Ruby threads and GIL limitations.

Documentation shows a sample usage :

rd, wr = IO.pipe

if fork 
  wr.close
  puts "Parent got: <#{rd.read}>"
  rd.close
  Process.wait
else 
  rd.close
  puts "Sending message to parent"
  wr.write "Hi Dad"
  wr.close
end

The fork call initiates two processes. Inside if, you are in the parent process. Inside else, you are in the child. The call to Process.wait closes child process. You can, for instance, try to read from your child in your main gui loop, and only close & wait for the child when you have received all the data.

EDIT: You'll need win32-process if you choose to use fork() under Windows.

Coren
  • 5,517
  • 1
  • 21
  • 34
  • Unfortunately, adding this line before `require 'tk'` changes absolutely nothing on both Windows and OSX. GUI still hangs badly. Have you checked this solution yourself? Maybe you can paste entire code here, maybe i miss something? – grigoryvp Feb 03 '12 at 09:41
  • Nope, I haven't tried myself this workaround. Did you try with jruby ? – Coren Feb 03 '12 at 09:52
  • And maybe the problem is with `loop` call. Did you try with a `while true`, like you did with Python ? – Coren Feb 03 '12 at 09:55
  • `while true` and `loop {}` is same in Ruby. i have used later in python due to absence of blocks in it. I have tried `while true` right now in Ruby, no difference :(. Also, i'm interested in Matz ruby interpreter that is installed on most user computers, not JRuby :) – grigoryvp Feb 03 '12 at 10:35
  • then you should look at Fiber and/or IO.pipe :) – Coren Feb 04 '12 at 12:12
  • Maybe you can write some sample code how fibers can help me? Actual application uses intensive XML processing in background thread via REXML - lags are not `so` big, but very visible and annoying to end users :( – grigoryvp Feb 04 '12 at 18:44
  • 2
    @EyeofHell not a solution, but a workaround: REXML is notoriously slow. If you were to switch to Nokogiri for XML processing, your lags would go away... – Mark Thomas Feb 07 '12 at 12:46
  • @Mark Thanks. I can `solve` the problem multiple ways, from using separate process to rewriting this tool in python. I'm curious `why` this problem exist. – grigoryvp Feb 07 '12 at 13:01
  • @EyeofHell Me too. Apparently some things don't release the GIL (ref: http://stackoverflow.com/a/9173549/182590). Could this be an issue? – Mark Thomas Feb 07 '12 at 13:07
  • @EyeofHell According to http://rtfblog.com/2011/05/25/taking-advantage-of-multithreaded-environments-with-ruby/ the more time spent in C code the better concurrency you get. More evidence supporting the use of Nokogiri (due to the libxml2 under the hood). – Mark Thomas Feb 07 '12 at 13:12
  • @Mark The problem is - TkRoot#mainloop `is` a `C` code :(. So in theory all must be smooth as silk. But reality is kind of harsh :( – grigoryvp Feb 07 '12 at 14:18
  • 1
    @EyeofHell C bindings with Tk are quite hard to read. It's not clear to me what kind of threads they use. Anyway, it means that IO.pipe should solve your refresh problem – Coren Feb 07 '12 at 15:23
  • @Coren How exactly IO.pipe should solve GUI hang problem? Maybe you can provide a code sample? – grigoryvp Feb 20 '12 at 21:42
  • @Coren ah, understood. You mean using process instead of thread and communicate via pipe/socket/xmlrpc/whatever. Yes, it's a workaround i'm using right now. But the main point of my question is to figure out *why* such behavior takes place :). Also, `fork` is not a very good solution since it will work on less than 10% computers out here running OSX or *NIX. Winows don't have `fork`, it uses threads as main concurrency building block. – grigoryvp Feb 22 '12 at 09:02
  • @EyeofHell fork can be available in Ruby Windows, with [win32-process](http://rubyforge.org/docman/view.php/85/1720/Process.html) – Coren Feb 22 '12 at 10:00
0

Your thread block will use 100% cpu, this is really unlikely any real code will eat that much (if you are doing really intensive calculations you should consider another language), maybe try adding some pauses:

require 'tk'
require 'thread'
require 'rexml/document'
Thread.new { loop { sleep 0.1; a = 1 } }
TkRoot.new.mainloop()

Your code works fine for me on Mac OS X 10.7 with 1.9.3 btw.

That said as much as I love ruby but the current gui libraries state is really bad in my opinion and I avoid using it for that.

Schmurfy
  • 1,715
  • 9
  • 17
  • In python code it's also 100% CPU load but no GUI freezes. Multithreading is all about executing multiple threads, regardless of CPU usage per thread. For example, if you start TWO ruby threads with same infinity loop, both will work perfectly in parallel, using 50% CPU each. So whu GUI is not working this way? – grigoryvp Jan 31 '12 at 10:19
  • About OSX 10.7 - just use resize instead of window move, OSX will move window without lags even if GUI is not responding, it's OS feature. – grigoryvp Jan 31 '12 at 10:20
  • I think what is happening is that the gui mainloop is in C, since your ruby code is taking a lot of cpu the application spend more time on the ruby side than on the C side. I never used tk but this hypotesis should be close to the truth. (I have no idea how it works on python and why it works differently with ruby maybe the library architecture is just different) – Schmurfy Jan 31 '12 at 15:59
  • 1
    AFAIK Ruby and Python uses same tcl/tk library. Anyway, situation is same with any toolkits i know: Tk, Qt, GTK etc. All works fine in python, terrible GUI lag in Ruby. My question is "why it is so" :). – grigoryvp Jan 31 '12 at 16:04
  • I was not speaking about the tk library by itself but more about the wrapper used to bridge ruby to C or Python to C but I never really experimented with C extension so I cannot give your more on that :/ – Schmurfy Jan 31 '12 at 16:22
0

Depending on platform you might set priority of threads:

require 'tk'
require 'thread'
require 'rexml/document'
t1 = Thread.new { loop { a = 1 } }
t1.priority = 0
t2 = TkRoot.new.mainloop()
t2.priority = 100
sunkencity
  • 3,482
  • 1
  • 22
  • 19
0

If you're serious about using multiple threads, you might want to consider using JRuby. It implements Ruby Threads using Java threads, giving you access to the Java concurrency libraries, tools, and well tested code.

For the most part, you just replace the ruby command with the jruby command.

Here's one place to start. https://github.com/jruby/jruby/wiki/Concurrency-in-jruby

Alex Blakemore
  • 11,301
  • 2
  • 26
  • 49
  • I know what is JRuby. I'm just curios what is happening - i can't understand the MRI behavior i observe O_O. It can't be like this if GIL is release. It can't be like this if GIL is not released. – grigoryvp Feb 14 '12 at 17:22