Most web applications are IO-intensive, and using Ruby multi-process + multi-threaded model will greatly improve system throughput. The reason is that when a certain Ruby thread is in the IO Block state, other threads can continue to execute. However, due to the existence of Ruby GIL (Global Interpreter Lock), MRI Ruby cannot truly utilize multithreading for parallel computing. JRuby removes GIL and is a truly multi-threading. It can not only cope with IO Blocks, but also make full use of multi-core CPUs to speed up the overall computing speed.
The above is relatively abstract, and the following are examples to illustrate it one by one.
Ruby multithreading and IO Block
Let’s first look at the following code (demonstration purpose, no practical purpose):
# File: block_io1.rb
def func1
puts "sleep 3 seconds in func1\n"
sleep(3)
end
def func2
puts "sleep 2 seconds in func2\n"
sleep(2)
end
def func3
puts "sleep 5 seconds in func3\n"
sleep(5)
end
func1
func2
func3
The code is very simple, with 3 methods, using sleep to simulate time-consuming IO operations. Running the code (Environment MRI Ruby 1.9.3) The result is:
$ time ruby block_io1.rb
sleep 3 seconds in func1
sleep 2 seconds in func2
sleep 5 seconds in func3
real 0m11.681s
user 0m3.086s
sys 0m0.152s
It's relatively slow, and it's all spent sleeping, and it took more than 10 seconds in total.
The multi-threading method is rewritten as follows:
# File: block_io2.rb
def func1
puts "sleep 3 seconds in func1\n"
sleep(3)
end
def func2
puts "sleep 2 seconds in func2\n"
sleep(2)
end
def func3
puts "sleep 5 seconds in func3\n"
sleep(5)
end
threads = []
threads << { func1 }
threads << { func2 }
threads << { func3 }
{ |t| }
The result of the operation is:
$ time ruby block_io2.rb
sleep 3 seconds in func1
sleep 2 seconds in func2
sleep 5 seconds in func3
real 0m6.543s
user 0m3.169s
sys 0m0.147s
It took more than 6 seconds in total, which was obviously much faster, only a little more than the longest sleep of 5 seconds.
The above example shows that Ruby's multi-threading can cope with IO Block. When a thread is in the IO Block state, other threads can continue to execute, thereby greatly shortening the overall processing time.
The impact of Ruby GIL
Let's look at a piece of code first (demo purpose):
# File:
require 'securerandom'
require 'zlib'
data = (4096000)
{ Zlib::(data) }
The code first generates some data randomly and then compresses it. The compression is very CPU-consuming. The running results in my machine (dual-core CPU, MRI Ruby 1.9.3) are as follows:
$ time ruby
real 0m8.572s
user 0m8.359s
sys 0m0.102s
Change to multithreaded version, the code is as follows:
# File:
require 'securerandom'
require 'zlib'
data = (4096000)
threads = []
do
threads << { Zlib::(data) }
end
{|t| }
The multi-threaded version run results are as follows:
$ time ruby
real 0m8.616s
user 0m8.377s
sys 0m0.211s
From the results, we can see that due to the existence of MRI Ruby GIL, Ruby multithreading cannot reuse multi-core CPUs. The overall time spent after using multi-threading is not shortened. On the contrary, due to the influence of thread switching, the time spent slightly increases.
JRuby removes GIL
Running with JRuby (JRuby 1.7.0 on my machine) and gets very different results.
$ time jruby
real 0m12.225s
user 0m14.060s
sys 0m0.615s
$ time jruby
real 0m7.584s
user 0m22.822s
sys 0m0.819s
As you can see, when JRuby uses multi-threading, the overall running time is significantly shortened (7.58 vs. 12.22). This is because JRuby removes GIL and can truly execute multi-threading in parallel, making full use of multi-core CPUs.
Summary: Ruby multithreading can still execute other threads when a certain thread IO Block, thereby reducing the overall impact of IO Block. However, due to the existence of MRI Ruby GIL, MRI Ruby is not really parallel execution. JRuby removes GIL and can achieve true multithreaded parallel execution.