Reputation: 2450
So I've got a reporter method running in a separate thread, it looks a little like this
def report
@reporters.each do |reporter|
Thread.new{ reporter.report }
end
end
The intention is that the reporter objects don't interfere with the performance of my main code loop. However this does create a lot of new threads, which seems to be taking up a lot of resources.
I'd like to instead collect those new threads and re-run them when the report method is called. Or send a single thread the report method on the reporter objects each time it's called.
Is there a way I can call methods in a separate thread after initialising it?
Upvotes: 0
Views: 123
Reputation: 19221
Although I side with @AlexeyShein that a tested library is the better option, allow me to raise a few thoughts/issues as well as offer a basic thread-pool demo.
The code I offer is similar to what I wrote for the Iodine project, although simpler and a bit less optimized.
Tasks in your thread pool might NOT complete:
When the hosting OS/server shuts your application down, it might not wait for all your tasks to complete.
The hosting OS/server will usually wait a short while (on Heroku it could be up to 30 seconds) before deciding your application had enough time and forcefully kills the process.
This is normal and expected behavior.
Tasks are NOT persistent:
Whenever your application restarts, the task queue will be a fresh and clean queue.
To resolve this issue, it is common to do two things: 1. Save the tasks in a database / module; and 2. run the background task manager in a different process (to avoid race conditions when scaling the application).
If you don't mind having incomplete tasks reinitiated after shutdown, here's a relatively simple thread-pool solution.
The following test code should show you how even small tasks might cause the rundown
to linger after exit was called. If your application will linger for long enough, it could be forcefully shut down by your host.
class ThreadPool
def initialize number_of_threads
@queue = Queue.new
@stop = false
@threads = []
number_of_threads.times do
@threads << Thread.new { work until @stop }
end
Kernel.at_exit { rundown }
end
def run &block
return false unless block
@queue << block
block
end
protected
def work
begin
@queue.pop.call
rescue => e
warn "A background task failed with #{e.message}"
end
end
def rundown
sleep 0.1 until @queue.empty?
stop
join
work until @queue.empty?
end
# will wait for all threads to finish
# if stop isn't called, this will hang forever!
def join
@threads.each {|t| t.join rescue true }
end
# will stop the thread pool and let the threads exit normally
def stop
@stop = true
(@threads.count * 2).times { run { nil } }
end
end
BK = ThreadPool.new 12
100.times {|i| BK.run {sleep 0.2; puts i.to_s} }
exit
Upvotes: 1
Reputation: 7482
Multithreading can be tricky, so I think the best option in your case would be to use some battle-tested library like sidekiq
that would take all heavy-lifting on itself.
Upvotes: 0
Reputation: 168081
That design pattern is called thread pool. There is plenty of information on the internet regarding Ruby implementation of thread pool.
Upvotes: 1