Class: SimpleThreadPool
- Inherits:
-
Object
- Object
- SimpleThreadPool
- Defined in:
- lib/simple_thread_pool.rb
Overview
Simple thread pool for executing blocks in parallel in a controlled manner. Threads are not re-used by the pool to prevent any thread local variables from leaking out.
Instance Method Summary collapse
-
#execute(id = nil) { ... } ⇒ void
Call this method to spawn a thread to run the block.
-
#finish ⇒ void
Call this method to block until all current threads have finished executing.
-
#initialize(max_threads) ⇒ SimpleThreadPool
constructor
A new instance of SimpleThreadPool.
-
#synchronize { ... } ⇒ Object
Synchronize data access across the thread pool.
Constructor Details
#initialize(max_threads) ⇒ SimpleThreadPool
Returns a new instance of SimpleThreadPool.
12 13 14 15 16 17 18 19 20 |
# File 'lib/simple_thread_pool.rb', line 12 def initialize(max_threads) raise ArgumentError, "max_threads must be at least 1" unless max_threads >= 1 @max_threads = max_threads @lock = Mutex.new @condition = ConditionVariable.new @threads = [] @processing_ids = [] @fatal_exception = nil end |
Instance Method Details
#execute(id = nil) { ... } ⇒ void
This method returns an undefined value.
Call this method to spawn a thread to run the block. If the thread pool is already full, this method will block until a thread is free. The block is responsible for handling any StandardError that could be raised.
If a block dies from an exception that is not a StandardError (i.e. an Exception that indicates the process itself is no longer healthy, such as NoMemoryError or SystemStackError), the pool is stopped. Blocks passed to this method after that point are not run; instead this method waits for the in-flight threads to finish and then raises the exception that stopped the pool, so that callers never silently lose work. The exception is cleared when it is raised, so it is delivered to exactly one caller and the pool can be used again afterwards.
The optional id argument can be used to provide an identifier for a block. If one is provided, processing will be blocked if the same id is already being processed. This ensures that each unique id is executed one at a time sequentially.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/simple_thread_pool.rb', line 43 def execute(id = nil, &block) loop do stopped = @lock.synchronize do until @fatal_exception || can_add_thread?(id) @condition.wait(@lock) end if @fatal_exception true else @processing_ids << id unless id.nil? thread_added = false begin add_thread(id, block) thread_added = true ensure unless thread_added @processing_ids.delete(id) unless id.nil? @condition.broadcast end end false end end break unless stopped # The pool has been stopped. Drain the in-flight threads and raise the # exception that stopped it. This has to happen outside of the synchronize # block above because #finish acquires the same lock. If another caller # claimed the exception first then #finish returns normally and the pool # is usable again, so retry rather than silently dropping the block. finish end nil end |
#finish ⇒ void
This method returns an undefined value.
Call this method to block until all current threads have finished executing.
Exceptions raised by the blocks are not propagated; they are the block's responsibility to handle. The exception is that if a block died from an exception that was not a StandardError, that exception is re-raised here after every in-flight thread has finished. In-flight threads are always allowed to run to completion rather than being killed, so that they can run their own ensure blocks. That exception is cleared as it is raised, so it is delivered to exactly one caller and the pool can be used again afterwards.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/simple_thread_pool.rb', line 92 def finish active_threads = @lock.synchronize do # Exclude the calling thread so that this is safe to call from inside a # block running in the pool; Thread#join raises if given the current # thread. #execute calls this method, so that is reachable indirectly. @threads.select { |thread| thread.alive? && !thread.equal?(Thread.current) } end active_threads.each do |thread| thread.join rescue Exception => e # standard:disable Lint/RescueException # Joining a thread that died with an unhandled exception re-raises that # exception; those are the block's responsibility to handle, so they are # not propagated here. Worker threads record the exception that killed # them, so anything else caught here was raised on the calling thread # (e.g. an Interrupt from a signal) and is always re-raised immediately so # that the calling thread stays responsive to signals. raise unless e.equal?(thread.thread_variable_get(WORKER_EXCEPTION)) end # Claim the exception under the lock and clear it before raising so that it # is delivered to exactly one caller. The pool is usable again afterwards. fatal_exception = @lock.synchronize do exception = @fatal_exception @fatal_exception = nil exception end raise fatal_exception if fatal_exception nil end |
#synchronize { ... } ⇒ Object
Synchronize data access across the thread pool. This method will block waiting on the same internal Mutex the thread pool uses to manage scheduling threads.
129 130 131 |
# File 'lib/simple_thread_pool.rb', line 129 def synchronize(&block) @lock.synchronize(&block) end |