vinted-thread 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
@@ -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