Process, Threads and Async Ruby
Bài đăng này đã không được cập nhật trong 4 năm
Ruby interpreter uses a single process by design. This means on your modern 8 core cpu, your script is going to use only 1/8th of your processing power at best. In this article, we'll see more about multi-threaded and multi-process ruby.
Using multiple threads
Matz ruby interpreter uses GIL (Global Interpreter Lock), thus it only lets one thread to run at a time. So, in cpu bound tasks, there is no benefit of using multi thread, and it'll yield no benefit for you (in ruby). First, let's take this example to calculate Fibonacci number. It's totally a cpu bound task.
def fib(n)
return n if [0,1].include?(n)
fib(n-1) + fib(n-2)
end
Benchmark.measure { 10.times { fib(35) } }
(CPU time|system CPU time|user and system CPU times|real time)
38.243695 0.647830 38.891525 ( 41.074481)
36.667084 0.550266 37.217350 ( 38.464907)
38.844508 0.711785 39.556293 ( 42.610056)
=>AVG: 40.72s
Let's run it now using 10 threads.
Benchmark.measure do
threads = []
10.times do
threads << Thread.new { Thread.current[:output] = fib(35) }
end
threads.each { |thread| thread.join }
end
On an ideal world, we'd hope tenfold performance increase. But,
38.623686 0.611559 39.235245 ( 40.751415)
38.077194 0.579472 38.656666 ( 39.956344)
38.445872 0.603536 39.049408 ( 40.273643)
=>AVG: 40.33s
So, what's the benefit of using threads then? The answer is, None (if you're trying to solve a cpu bound problem). But, if you're trying to solve an IO bound issue, then threads will speedup your performance a lot.
Example: Performing HTTP requests with multiple threads
Imagine a scenario, where we have a method that checks if it can access some websites and responds back with HTTP status code.
require 'benchmark'
require 'net/http'
def check servers
servers.each do |server|
response = Net::HTTP.get_response(server, '/')
puts server, response.code
end
end
SERVERS = Array.new(100, "www.google.com")
puts Benchmark.measure {check(SERVERS)}
ruby thread.rb
0.078843 0.046223 0.125066 ( 27.263542)
Now, let's rewrite the code to use multiple threads to do the job
def check servers
threads = []
servers.each do |server|
threads << Thread.new {
response = Net::HTTP.get_response(server, '/')
puts server, response.code
}
end
threads.each { |thread| thread.join }
end
0.094302 0.038597 0.132899 ( 1.383422)
That is a huge improvement over our single threaded implementation. On the plus side, running multiple threads don't increase memory usage exponentialy like using multi-process.
Benefits
- Speedup for blocking operations
- Variables can be shared/modified (beaware of deadlocks)
- No extra memory used
Cons
- Much harder to debug
Using multiple processes
Remember our fibonacci implementation from #threads section.
def fib(n)
return n if [0,1].include?(n)
fib(n-1) + fib(n-2)
end
Benchmark.measure { 10.times { fib(35) } }
(CPU time|system CPU time|user and system CPU times|real time)
38.243695 0.647830 38.891525 ( 41.074481)
36.667084 0.550266 37.217350 ( 38.464907)
38.844508 0.711785 39.556293 ( 42.610056)
=>AVG: 40.72s
We'll now try to run this with multiple process instead of threads. The re-written function will be
Benchmark.measure {
read_stream, write_stream = IO.pipe
10.times do
Process.fork do
write_stream.puts fib(35)
end
end
Process.waitall
write_stream.close
results = read_stream.read
read_stream.close
}
now, let's see the benchmark.
0.001240 0.005190 63.827237 ( 17.158324)
0.001579 0.007635 65.032995 ( 19.821757)
0.001433 0.006900 64.022068 ( 18.152649)
=>AVG: 18.38s
So, compared to 40 sec, it's using 18 sec. Which is a great improvement. Note the memory usage
This implementation is using 10 times higher memory. Which is the tradeoff.
Benefits
- Speedup through multiple CPUs
- Speedup for blocking operations
- Variables are protected from change
- Child processes are killed when your main process is killed through Ctrl+c or kill -2
Cons
- Memory usage will be higher.
Summary
It's best described in this excellent article from Eqbal Quran .
In conclusion, we can say, there is not end all be all solution on which is best. We have to understand the workload and choose the best solution for our problem.
Things to study
All rights reserved