Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why Ruby 1.9 GUI hangs if i do any intensive computation in separate Ruby thread?

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
like image 328
grigoryvp Avatar asked Jan 30 '12 12:01

grigoryvp


People also ask

How many threads can ruby handle?

The Ruby interpreter handles the management of the threads and only one or two native thread are created.

Is Ruby multithreaded or single threaded?

The Ruby Interpreter is single threaded, which is to say that several of its methods are not thread safe. In the Rails world, this single-thread has mostly been pushed to the server.

Does Ruby support multithreading?

Multi-threading is the most useful property of Ruby which allows concurrent programming of two or more parts of the program for maximizing the utilization of CPU. Each part of a program is called Thread. So, in other words, threads are lightweight processes within a process.

What does thread join do in Ruby?

Calling Thread. join blocks the current (main) thread. However not calling join results in all spawned threads to be killed when the main thread exits.


1 Answers

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.

like image 177
Coren Avatar answered Dec 10 '22 15:12

Coren