Multithreading in Ruby on Rails
Don't you hate it when sites say "Please Wait" when you'd rather just come back later? I am always worried my browser will close and it won't work. Or maybe I want to shut my computer down but I have to leave my task running. Read on!
Another possible use case is that you would like to do many tasks in parallel. Consider thumbnailing via imagemagick. Maybe you want to make eight different thumbnails, and maybe you're lucky enough to have a multi-core server. Shouldn't you use them?
Let's tackle this problem first. Multithreading in Ruby on Rails is actually very simple. Let's say you have a function "my_stuff":
def my_stuff(thing)
puts thing
end
And somewhere else we want to do my_stuff on a bunch of things:
threads = []
['a', 'b', 'c'].each do |letter|
threads << Thread.new { my_stuff letter }
end
threads.each do |t|
t.join
end
puts "All Done"
Let's break this down a little. For each of the letters we'll spawn a new thread. This Thread.new returns a "thread", which we add to our threads array. Once we have spun off each thread, we do a loop over the threads array to join them. Join means "sit here until it finishes". So they all run at the same time, then they all merge back together, then we know we are done.
So that's nice, but we want to do more. We want to fork off a different process instead of just spawning a new thread. Why? Because we want this process to be independent from the parent. This means that they won't be sharing memory. This is a good and a bad thing. It's bad because now we have to copy the process when we fork. It's good because now we can do a lot of mucking around without disturbing the other threads.
In my project, I needed to fork because each process was working in a different directory, and they would keep changing each others directories, and everything would mess up.
Let's see how we would do the above example with forking:
children = []
['a', 'b', 'c'].each do |letter|
children << fork { my_stuff letter }
end
children.each do |c|
wait(c)
end
puts "All Done"
Pretty much the same huh?
So how about the beginning of this post, where I talked about "Please Wait"? Aren't we still waiting? This is where Process.detach comes in. When we wait, we are expressing interest in our child process. When we detach we are explicitly expressing disintrest in our child (poor kid). So let's go to that thumbnailing example:
def create_thumbnail(source, destination, size)
`convert -thumbnail #{size} #{source} #{destination}`
end
# lets pretend source = "public/images/mypic" for the file "public/images/mypic.png"
def make_all_thumbs(source)
sizes = ['25x25', '50x50', '75x75', '100x100']
sizes.each do |s|
child = fork { create_thumbnail(source+'.png', source+'-'+s+'.png', s) }
Process.detach child if child
end
end
There we go. That all makes sense, but what's up with "if child"? How can it not make a child? Well remember when I said it copies the memory of the main thread? It copies everything, meaning that the child is executing just like its parent. The only difference is that in the parent, child = the pid of the child, but in the child, child = nil. So it's basically saying "detach the child if I'm the parent".
Now, the user gets an instant response after each process is started. Their thumbnail images will probably come up blank, but when they visit later and all the thumbs are created, they will show up.
One caveat on all this: beware database actions! If you try to access the database simultaneously, things crash. I haven't had the chance to tackle this problem yet, so you're on your own. If you know how, please comment. I'm guessing it involves mutexes.
Want to learn more from Smartlogic, a ruby on rails development firm, follow us on Twitter