vinted-thread 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.travis.yml +7 -0
- data/README.md +195 -0
- data/Rakefile +21 -0
- data/lib/thread/channel.rb +105 -0
- data/lib/thread/delay.rb +94 -0
- data/lib/thread/every.rb +197 -0
- data/lib/thread/future.rb +158 -0
- data/lib/thread/pipe.rb +119 -0
- data/lib/thread/pool.rb +428 -0
- data/lib/thread/process.rb +72 -0
- data/lib/thread/promise.rb +83 -0
- data/lib/thread/recursive_mutex.rb +38 -0
- data/tests/channel_spec.rb +41 -0
- data/tests/delay_spec.rb +13 -0
- data/tests/every_spec.rb +11 -0
- data/tests/future_spec.rb +37 -0
- data/tests/pipe_spec.rb +25 -0
- data/tests/promise_spec.rb +37 -0
- data/thread.gemspec +18 -0
- metadata +90 -0
@@ -0,0 +1,158 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'thread'
|
12
|
+
|
13
|
+
# A future is an object that incapsulates a block which is called in a
|
14
|
+
# different thread, upon retrieval the caller gets blocked until the block has
|
15
|
+
# finished running, and its result is returned and cached.
|
16
|
+
class Thread::Future
|
17
|
+
Cancel = Class.new(Exception)
|
18
|
+
|
19
|
+
# Create a future with the passed block and optionally using the passed pool.
|
20
|
+
def initialize (pool = nil, &block)
|
21
|
+
raise ArgumentError, 'no block given' unless block
|
22
|
+
|
23
|
+
@mutex = Mutex.new
|
24
|
+
|
25
|
+
task = proc {
|
26
|
+
begin
|
27
|
+
deliver block.call
|
28
|
+
rescue Exception => e
|
29
|
+
@exception = e
|
30
|
+
|
31
|
+
deliver nil
|
32
|
+
end
|
33
|
+
}
|
34
|
+
|
35
|
+
@thread = pool ? pool.process(&task) : Thread.new(&task)
|
36
|
+
|
37
|
+
ObjectSpace.define_finalizer self, self.class.finalizer(@thread)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @private
|
41
|
+
def self.finalizer (thread)
|
42
|
+
proc {
|
43
|
+
thread.raise Cancel.new
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if an exception has been raised.
|
48
|
+
def exception?
|
49
|
+
@mutex.synchronize {
|
50
|
+
instance_variable_defined? :@exception
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return the raised exception.
|
55
|
+
def exception
|
56
|
+
@mutex.synchronize {
|
57
|
+
@exception
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if the future has been called.
|
62
|
+
def delivered?
|
63
|
+
@mutex.synchronize {
|
64
|
+
instance_variable_defined? :@value
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
alias realized? delivered?
|
69
|
+
|
70
|
+
# Cancel the future, {#value} will yield a Cancel exception
|
71
|
+
def cancel
|
72
|
+
return self if delivered?
|
73
|
+
|
74
|
+
@mutex.synchronize {
|
75
|
+
@thread.raise Cancel.new('future cancelled')
|
76
|
+
}
|
77
|
+
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check if the future has been cancelled
|
82
|
+
def cancelled?
|
83
|
+
@mutex.synchronize {
|
84
|
+
@exception.is_a? Cancel
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get the value of the future, if it's not finished running this call will block.
|
89
|
+
#
|
90
|
+
# In case the block raises an exception, it will be raised, the exception is cached
|
91
|
+
# and will be raised every time you access the value.
|
92
|
+
#
|
93
|
+
# An optional timeout can be passed which will return nil if nothing has been
|
94
|
+
# delivered.
|
95
|
+
def value (timeout = nil)
|
96
|
+
raise @exception if exception?
|
97
|
+
|
98
|
+
return @value if delivered?
|
99
|
+
|
100
|
+
@mutex.synchronize {
|
101
|
+
cond.wait(@mutex, *timeout)
|
102
|
+
}
|
103
|
+
|
104
|
+
if exception?
|
105
|
+
raise @exception
|
106
|
+
elsif delivered?
|
107
|
+
return @value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
alias ~ value
|
112
|
+
|
113
|
+
# Do the same as {#value}, but return nil in case of exception.
|
114
|
+
def value! (timeout = nil)
|
115
|
+
begin
|
116
|
+
value(timeout)
|
117
|
+
rescue Exception
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
alias ! value!
|
123
|
+
|
124
|
+
private
|
125
|
+
def cond?
|
126
|
+
instance_variable_defined? :@cond
|
127
|
+
end
|
128
|
+
|
129
|
+
def cond
|
130
|
+
@cond ||= ConditionVariable.new
|
131
|
+
end
|
132
|
+
|
133
|
+
def deliver (value)
|
134
|
+
return if delivered?
|
135
|
+
|
136
|
+
@mutex.synchronize {
|
137
|
+
@value = value
|
138
|
+
|
139
|
+
cond.broadcast if cond?
|
140
|
+
}
|
141
|
+
|
142
|
+
self
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class Thread
|
147
|
+
# Helper to create a future
|
148
|
+
def self.future (pool = nil, &block)
|
149
|
+
Thread::Future.new(pool, &block)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
module Kernel
|
154
|
+
# Helper to create a future.
|
155
|
+
def future (pool = nil, &block)
|
156
|
+
Thread::Future.new(pool, &block)
|
157
|
+
end
|
158
|
+
end
|
data/lib/thread/pipe.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'thread'
|
12
|
+
|
13
|
+
# A pipe lets you execute various tasks on a set of data in parallel,
|
14
|
+
# each datum inserted in the pipe is passed along through queues to the various
|
15
|
+
# functions composing the pipe, the final result is inserted in the final queue.
|
16
|
+
class Thread::Pipe
|
17
|
+
# A task incapsulates a part of the pipe.
|
18
|
+
class Task
|
19
|
+
attr_accessor :input, :output
|
20
|
+
|
21
|
+
# Create a Task which will call the passed function and get input
|
22
|
+
# from the optional parameter and put output in the optional parameter.
|
23
|
+
def initialize (func, input = Queue.new, output = Queue.new)
|
24
|
+
@input = input
|
25
|
+
@output = output
|
26
|
+
@handling = false
|
27
|
+
|
28
|
+
@thread = Thread.new {
|
29
|
+
while true
|
30
|
+
value = @input.deq
|
31
|
+
|
32
|
+
@handling = true
|
33
|
+
begin
|
34
|
+
value = func.call(value)
|
35
|
+
@output.enq value
|
36
|
+
rescue Exception; end
|
37
|
+
@handling = false
|
38
|
+
end
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check if the task has nothing to do.
|
43
|
+
def empty?
|
44
|
+
!@handling && @input.empty? && @output.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Stop the task.
|
48
|
+
def kill
|
49
|
+
@thread.raise
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create a pipe using the optionally passed objects as input and
|
54
|
+
# output queue.
|
55
|
+
#
|
56
|
+
# The objects must respond to #enq and #deq, and block on #deq.
|
57
|
+
def initialize (input = Queue.new, output = Queue.new)
|
58
|
+
@tasks = []
|
59
|
+
|
60
|
+
@input = input
|
61
|
+
@output = output
|
62
|
+
|
63
|
+
ObjectSpace.define_finalizer self, self.class.finalizer(@tasks)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @private
|
67
|
+
def self.finalizer (tasks)
|
68
|
+
proc {
|
69
|
+
tasks.each(&:kill)
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add a task to the pipe, it must respond to #call and #arity,
|
74
|
+
# and #arity must return 1.
|
75
|
+
def | (func)
|
76
|
+
if func.arity != 1
|
77
|
+
raise ArgumentError, 'wrong arity'
|
78
|
+
end
|
79
|
+
|
80
|
+
Task.new(func, (@tasks.empty? ? @input : Queue.new), @output).tap {|t|
|
81
|
+
@tasks.last.output = t.input unless @tasks.empty?
|
82
|
+
@tasks << t
|
83
|
+
}
|
84
|
+
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
# Check if the pipe is empty.
|
89
|
+
def empty?
|
90
|
+
@input.empty? && @output.empty? && @tasks.all?(&:empty?)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Insert data in the pipe.
|
94
|
+
def enq (data)
|
95
|
+
return if @tasks.empty?
|
96
|
+
|
97
|
+
@input.enq data
|
98
|
+
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
alias push enq
|
103
|
+
alias << enq
|
104
|
+
|
105
|
+
# Get an element from the output queue.
|
106
|
+
def deq (non_block = false)
|
107
|
+
@output.deq(non_block)
|
108
|
+
end
|
109
|
+
|
110
|
+
alias pop deq
|
111
|
+
alias ~ deq
|
112
|
+
end
|
113
|
+
|
114
|
+
class Thread
|
115
|
+
# Helper to create a pipe.
|
116
|
+
def self.| (func)
|
117
|
+
Pipe.new | func
|
118
|
+
end
|
119
|
+
end
|
data/lib/thread/pool.rb
ADDED
@@ -0,0 +1,428 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'thread'
|
12
|
+
|
13
|
+
# A pool is a container of a limited amount of threads to which you can add
|
14
|
+
# tasks to run.
|
15
|
+
#
|
16
|
+
# This is usually more performant and less memory intensive than creating a
|
17
|
+
# new thread for every task.
|
18
|
+
class Thread::Pool
|
19
|
+
# A task incapsulates a block being ran by the pool and the arguments to pass
|
20
|
+
# to it.
|
21
|
+
class Task
|
22
|
+
Timeout = Class.new(Exception)
|
23
|
+
Asked = Class.new(Exception)
|
24
|
+
|
25
|
+
attr_reader :pool, :timeout, :exception, :thread, :started_at
|
26
|
+
|
27
|
+
# Create a task in the given pool which will pass the arguments to the
|
28
|
+
# block.
|
29
|
+
def initialize (pool, *args, &block)
|
30
|
+
@pool = pool
|
31
|
+
@arguments = args
|
32
|
+
@block = block
|
33
|
+
|
34
|
+
@running = false
|
35
|
+
@finished = false
|
36
|
+
@timedout = false
|
37
|
+
@terminated = false
|
38
|
+
end
|
39
|
+
|
40
|
+
def running?; @running; end
|
41
|
+
def finished?; @finished; end
|
42
|
+
def timeout?; @timedout; end
|
43
|
+
def terminated?; @terminated; end
|
44
|
+
|
45
|
+
# Execute the task in the given thread.
|
46
|
+
def execute (thread)
|
47
|
+
return if terminated? || running? || finished?
|
48
|
+
|
49
|
+
@thread = thread
|
50
|
+
@running = true
|
51
|
+
@started_at = Time.now
|
52
|
+
|
53
|
+
pool.__send__ :wake_up_timeout
|
54
|
+
|
55
|
+
begin
|
56
|
+
@block.call(*@arguments)
|
57
|
+
rescue Exception => reason
|
58
|
+
if reason.is_a? Timeout
|
59
|
+
@timedout = true
|
60
|
+
elsif reason.is_a? Asked
|
61
|
+
return
|
62
|
+
else
|
63
|
+
@exception = reason
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
@running = false
|
68
|
+
@finished = true
|
69
|
+
@thread = nil
|
70
|
+
end
|
71
|
+
|
72
|
+
# Raise an exception in the thread used by the task.
|
73
|
+
def raise (exception)
|
74
|
+
@thread.raise(exception)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Terminate the exception with an optionally given exception.
|
78
|
+
def terminate! (exception = Asked)
|
79
|
+
return if terminated? || finished? || timeout?
|
80
|
+
|
81
|
+
@terminated = true
|
82
|
+
|
83
|
+
return unless running?
|
84
|
+
|
85
|
+
self.raise exception
|
86
|
+
end
|
87
|
+
|
88
|
+
# Force the task to timeout.
|
89
|
+
def timeout!
|
90
|
+
terminate! Timeout
|
91
|
+
end
|
92
|
+
|
93
|
+
# Timeout the task after the given time.
|
94
|
+
def timeout_after (time)
|
95
|
+
@timeout = time
|
96
|
+
|
97
|
+
pool.timeout_for self, time
|
98
|
+
|
99
|
+
self
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
attr_reader :min, :max, :spawned
|
104
|
+
|
105
|
+
# Create the pool with minimum and maximum threads.
|
106
|
+
#
|
107
|
+
# The pool will start with the minimum amount of threads created and will
|
108
|
+
# spawn new threads until the max is reached in case of need.
|
109
|
+
#
|
110
|
+
# A default block can be passed, which will be used to {#process} the passed
|
111
|
+
# data.
|
112
|
+
def initialize (min, max = nil, &block)
|
113
|
+
@min = min
|
114
|
+
@max = max || min
|
115
|
+
@block = block
|
116
|
+
|
117
|
+
@cond = ConditionVariable.new
|
118
|
+
@mutex = Mutex.new
|
119
|
+
|
120
|
+
@done = ConditionVariable.new
|
121
|
+
@done_mutex = Mutex.new
|
122
|
+
|
123
|
+
@todo = []
|
124
|
+
@workers = []
|
125
|
+
@timeouts = {}
|
126
|
+
|
127
|
+
@spawned = 0
|
128
|
+
@waiting = 0
|
129
|
+
@shutdown = false
|
130
|
+
@trim_requests = 0
|
131
|
+
@auto_trim = false
|
132
|
+
@idle_trim = nil
|
133
|
+
|
134
|
+
@mutex.synchronize {
|
135
|
+
min.times {
|
136
|
+
spawn_thread
|
137
|
+
}
|
138
|
+
}
|
139
|
+
end
|
140
|
+
|
141
|
+
# Check if the pool has been shut down.
|
142
|
+
def shutdown?; !!@shutdown; end
|
143
|
+
|
144
|
+
# Check if auto trimming is enabled.
|
145
|
+
def auto_trim?
|
146
|
+
@auto_trim
|
147
|
+
end
|
148
|
+
|
149
|
+
# Enable auto trimming, unneeded threads will be deleted until the minimum
|
150
|
+
# is reached.
|
151
|
+
def auto_trim!
|
152
|
+
@auto_trim = true
|
153
|
+
end
|
154
|
+
|
155
|
+
# Disable auto trimming.
|
156
|
+
def no_auto_trim!
|
157
|
+
@auto_trim = false
|
158
|
+
end
|
159
|
+
|
160
|
+
# Check if idle trimming is enabled.
|
161
|
+
def idle_trim?
|
162
|
+
!@idle_trim.nil?
|
163
|
+
end
|
164
|
+
|
165
|
+
# Enable idle trimming. Unneeded threads will be deleted after the given number of seconds of inactivity.
|
166
|
+
# The minimum number of threads is respeced.
|
167
|
+
def idle_trim!(timeout)
|
168
|
+
@idle_trim = timeout
|
169
|
+
end
|
170
|
+
|
171
|
+
# Turn of idle trimming.
|
172
|
+
def no_idle_trim!
|
173
|
+
@idle_trim = nil
|
174
|
+
end
|
175
|
+
|
176
|
+
# Resize the pool with the passed arguments.
|
177
|
+
def resize (min, max = nil)
|
178
|
+
@min = min
|
179
|
+
@max = max || min
|
180
|
+
|
181
|
+
trim!
|
182
|
+
end
|
183
|
+
|
184
|
+
# Get the amount of tasks that still have to be run.
|
185
|
+
def backlog
|
186
|
+
@mutex.synchronize {
|
187
|
+
@todo.length
|
188
|
+
}
|
189
|
+
end
|
190
|
+
|
191
|
+
# Are all tasks consumed ?
|
192
|
+
def done?
|
193
|
+
@todo.empty? and @waiting == @spawned
|
194
|
+
end
|
195
|
+
|
196
|
+
# Wait until all tasks are consumed. The caller will be blocked until then.
|
197
|
+
def wait_done
|
198
|
+
@done_mutex.synchronize {
|
199
|
+
return if done?
|
200
|
+
@done.wait @done_mutex
|
201
|
+
}
|
202
|
+
end
|
203
|
+
|
204
|
+
# Add a task to the pool which will execute the block with the given
|
205
|
+
# argument.
|
206
|
+
#
|
207
|
+
# If no block is passed the default block will be used if present, an
|
208
|
+
# ArgumentError will be raised otherwise.
|
209
|
+
def process (*args, &block)
|
210
|
+
unless block || @block
|
211
|
+
raise ArgumentError, 'you must pass a block'
|
212
|
+
end
|
213
|
+
|
214
|
+
task = Task.new(self, *args, &(block || @block))
|
215
|
+
|
216
|
+
@mutex.synchronize {
|
217
|
+
raise 'unable to add work while shutting down' if shutdown?
|
218
|
+
|
219
|
+
@todo << task
|
220
|
+
|
221
|
+
if @waiting == 0 && @spawned < @max
|
222
|
+
spawn_thread
|
223
|
+
end
|
224
|
+
|
225
|
+
@cond.signal
|
226
|
+
}
|
227
|
+
|
228
|
+
task
|
229
|
+
end
|
230
|
+
|
231
|
+
alias << process
|
232
|
+
|
233
|
+
# Trim the unused threads, if forced threads will be trimmed even if there
|
234
|
+
# are tasks waiting.
|
235
|
+
def trim (force = false)
|
236
|
+
@mutex.synchronize {
|
237
|
+
if (force || @waiting > 0) && @spawned - @trim_requests > @min
|
238
|
+
@trim_requests += 1
|
239
|
+
@cond.signal
|
240
|
+
end
|
241
|
+
}
|
242
|
+
|
243
|
+
self
|
244
|
+
end
|
245
|
+
|
246
|
+
# Force #{trim}.
|
247
|
+
def trim!
|
248
|
+
trim true
|
249
|
+
end
|
250
|
+
|
251
|
+
# Shut down the pool instantly without finishing to execute tasks.
|
252
|
+
def shutdown!
|
253
|
+
@mutex.synchronize {
|
254
|
+
@shutdown = :now
|
255
|
+
@cond.broadcast
|
256
|
+
}
|
257
|
+
|
258
|
+
wake_up_timeout
|
259
|
+
|
260
|
+
self
|
261
|
+
end
|
262
|
+
|
263
|
+
# Shut down the pool, it will block until all tasks have finished running.
|
264
|
+
def shutdown
|
265
|
+
@mutex.synchronize {
|
266
|
+
@shutdown = :nicely
|
267
|
+
@cond.broadcast
|
268
|
+
}
|
269
|
+
|
270
|
+
join
|
271
|
+
|
272
|
+
if @timeout
|
273
|
+
@shutdown = :now
|
274
|
+
|
275
|
+
wake_up_timeout
|
276
|
+
|
277
|
+
@timeout.join
|
278
|
+
end
|
279
|
+
|
280
|
+
self
|
281
|
+
end
|
282
|
+
|
283
|
+
# Join on all threads in the pool.
|
284
|
+
def join
|
285
|
+
until @workers.empty?
|
286
|
+
if worker = @workers.first
|
287
|
+
worker.join
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
self
|
292
|
+
end
|
293
|
+
|
294
|
+
# Define a timeout for a task.
|
295
|
+
def timeout_for (task, timeout)
|
296
|
+
unless @timeout
|
297
|
+
spawn_timeout_thread
|
298
|
+
end
|
299
|
+
|
300
|
+
@mutex.synchronize {
|
301
|
+
@timeouts[task] = timeout
|
302
|
+
|
303
|
+
wake_up_timeout
|
304
|
+
}
|
305
|
+
end
|
306
|
+
|
307
|
+
# Shutdown the pool after a given amount of time.
|
308
|
+
def shutdown_after (timeout)
|
309
|
+
Thread.new {
|
310
|
+
sleep timeout
|
311
|
+
|
312
|
+
shutdown
|
313
|
+
}
|
314
|
+
|
315
|
+
self
|
316
|
+
end
|
317
|
+
|
318
|
+
private
|
319
|
+
def wake_up_timeout
|
320
|
+
if defined? @pipes
|
321
|
+
@pipes.last.write_nonblock 'x' rescue nil
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def spawn_thread
|
326
|
+
@spawned += 1
|
327
|
+
|
328
|
+
thread = Thread.new {
|
329
|
+
loop do
|
330
|
+
task = @mutex.synchronize {
|
331
|
+
if @todo.empty?
|
332
|
+
while @todo.empty?
|
333
|
+
if @trim_requests > 0
|
334
|
+
@trim_requests -= 1
|
335
|
+
|
336
|
+
break
|
337
|
+
end
|
338
|
+
|
339
|
+
break if shutdown?
|
340
|
+
|
341
|
+
@waiting += 1
|
342
|
+
|
343
|
+
report_done
|
344
|
+
|
345
|
+
if @idle_trim and @spawned > @min
|
346
|
+
check_time = Time.now + @idle_trim
|
347
|
+
@cond.wait @mutex, @idle_trim
|
348
|
+
@trim_requests += 1 if Time.now >= check_time && @spawned - @trim_requests > @min
|
349
|
+
else
|
350
|
+
@cond.wait @mutex
|
351
|
+
end
|
352
|
+
|
353
|
+
@waiting -= 1
|
354
|
+
end
|
355
|
+
|
356
|
+
break if @todo.empty? && shutdown?
|
357
|
+
end
|
358
|
+
|
359
|
+
@todo.shift
|
360
|
+
} or break
|
361
|
+
|
362
|
+
task.execute(thread)
|
363
|
+
|
364
|
+
break if @shutdown == :now
|
365
|
+
|
366
|
+
trim if auto_trim? && @spawned > @min
|
367
|
+
end
|
368
|
+
|
369
|
+
@mutex.synchronize {
|
370
|
+
@spawned -= 1
|
371
|
+
@workers.delete thread
|
372
|
+
}
|
373
|
+
}
|
374
|
+
|
375
|
+
@workers << thread
|
376
|
+
|
377
|
+
thread
|
378
|
+
end
|
379
|
+
|
380
|
+
def spawn_timeout_thread
|
381
|
+
@pipes = IO.pipe
|
382
|
+
@timeout = Thread.new {
|
383
|
+
loop do
|
384
|
+
now = Time.now
|
385
|
+
timeout = @timeouts.map {|task, time|
|
386
|
+
next unless task.started_at
|
387
|
+
|
388
|
+
now - task.started_at + task.timeout
|
389
|
+
}.compact.min unless @timeouts.empty?
|
390
|
+
|
391
|
+
readable, = IO.select([@pipes.first], nil, nil, timeout)
|
392
|
+
|
393
|
+
break if @shutdown == :now
|
394
|
+
|
395
|
+
if readable && !readable.empty?
|
396
|
+
readable.first.read_nonblock 1024
|
397
|
+
end
|
398
|
+
|
399
|
+
now = Time.now
|
400
|
+
@timeouts.each {|task, time|
|
401
|
+
next if !task.started_at || task.terminated? || task.finished?
|
402
|
+
|
403
|
+
if now > task.started_at + task.timeout
|
404
|
+
task.timeout!
|
405
|
+
end
|
406
|
+
}
|
407
|
+
|
408
|
+
@timeouts.reject! { |task, _| task.terminated? || task.finished? }
|
409
|
+
|
410
|
+
break if @shutdown == :now
|
411
|
+
end
|
412
|
+
}
|
413
|
+
end
|
414
|
+
|
415
|
+
def report_done
|
416
|
+
@done_mutex.synchronize {
|
417
|
+
@done.broadcast if done?
|
418
|
+
}
|
419
|
+
end
|
420
|
+
|
421
|
+
end
|
422
|
+
|
423
|
+
class Thread
|
424
|
+
# Helper to create a pool.
|
425
|
+
def self.pool (*args, &block)
|
426
|
+
Thread::Pool.new(*args, &block)
|
427
|
+
end
|
428
|
+
end
|