thread 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +18 -2
- data/lib/thread/channel.rb +15 -0
- data/lib/thread/delay.rb +73 -0
- data/lib/thread/future.rb +88 -2
- data/lib/thread/pool.rb +58 -9
- data/lib/thread/promise.rb +8 -1
- data/lib/thread/recursive_mutex.rb +6 -0
- data/thread.gemspec +3 -3
- metadata +5 -3
data/README.md
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
thread - various extensions to the thread
|
2
|
-
|
1
|
+
thread - various extensions to the thread library
|
2
|
+
=================================================
|
3
3
|
|
4
4
|
Pool
|
5
5
|
====
|
@@ -108,3 +108,19 @@ puts ~future {
|
|
108
108
|
42
|
109
109
|
} # => 42
|
110
110
|
```
|
111
|
+
|
112
|
+
Delay
|
113
|
+
=====
|
114
|
+
A delay is kind of a promise, except the block is called when the value is
|
115
|
+
being accessed and the result is cached.
|
116
|
+
|
117
|
+
Example
|
118
|
+
-------
|
119
|
+
|
120
|
+
```ruby
|
121
|
+
require 'thread/delay'
|
122
|
+
|
123
|
+
puts ~delay {
|
124
|
+
42
|
125
|
+
}
|
126
|
+
```
|
data/lib/thread/channel.rb
CHANGED
@@ -10,7 +10,12 @@
|
|
10
10
|
|
11
11
|
require 'thread'
|
12
12
|
|
13
|
+
# A channel lets you send and receive various messages in a thread-safe way.
|
14
|
+
#
|
15
|
+
# It also allows for guards upon sending and retrieval, to ensure the passed
|
16
|
+
# messages are safe to be consumed.
|
13
17
|
class Thread::Channel
|
18
|
+
# Create a channel with optional initial messages and optional channel guard.
|
14
19
|
def initialize (messages = [], &block)
|
15
20
|
@messages = []
|
16
21
|
@mutex = Mutex.new
|
@@ -22,6 +27,10 @@ class Thread::Channel
|
|
22
27
|
}
|
23
28
|
end
|
24
29
|
|
30
|
+
# Send a message to the channel.
|
31
|
+
#
|
32
|
+
# If there's a guard, the value is passed to it, if the guard returns a falsy value
|
33
|
+
# an ArgumentError exception is raised and the message is not sent.
|
25
34
|
def send (what)
|
26
35
|
if @check && !@check.call(what)
|
27
36
|
raise ArgumentError, 'guard mismatch'
|
@@ -35,6 +44,9 @@ class Thread::Channel
|
|
35
44
|
self
|
36
45
|
end
|
37
46
|
|
47
|
+
# Receive a message, if there are none the call blocks until there's one.
|
48
|
+
#
|
49
|
+
# If a block is passed, it's used as guard to match to a message.
|
38
50
|
def receive (&block)
|
39
51
|
message = nil
|
40
52
|
|
@@ -64,6 +76,9 @@ class Thread::Channel
|
|
64
76
|
message
|
65
77
|
end
|
66
78
|
|
79
|
+
# Receive a message, if there are none the call returns nil.
|
80
|
+
#
|
81
|
+
# If a block is passed, it's used as guard to match to a message.
|
67
82
|
def receive! (&block)
|
68
83
|
if block
|
69
84
|
@messages.delete_at(@messages.find_index(&block))
|
data/lib/thread/delay.rb
ADDED
@@ -0,0 +1,73 @@
|
|
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
|
+
# A delay is an object that incapsulates a block which is called upon
|
12
|
+
# value retrieval, and its result cached.
|
13
|
+
class Thread::Delay
|
14
|
+
def initialize (&block)
|
15
|
+
@block = block
|
16
|
+
end
|
17
|
+
|
18
|
+
# Check if an exception has been raised.
|
19
|
+
def exception?
|
20
|
+
instance_variable_defined? :@exception
|
21
|
+
end
|
22
|
+
|
23
|
+
# Return the raised exception.
|
24
|
+
def exception
|
25
|
+
@exception
|
26
|
+
end
|
27
|
+
|
28
|
+
# Check if the delay has been called.
|
29
|
+
def delivered?
|
30
|
+
instance_variable_defined? :@value
|
31
|
+
end
|
32
|
+
|
33
|
+
alias realized? delivered?
|
34
|
+
|
35
|
+
# Get the value of the delay, if it's already been executed, return the
|
36
|
+
# cached result, otherwise execute the block and return the value.
|
37
|
+
#
|
38
|
+
# In case the block raises an exception, it will be raised, the exception is
|
39
|
+
# cached and will be raised every time you access the value.
|
40
|
+
def value
|
41
|
+
raise @exception if exception?
|
42
|
+
|
43
|
+
return @value if realized?
|
44
|
+
|
45
|
+
begin
|
46
|
+
@value = @block.call
|
47
|
+
rescue Exception => e
|
48
|
+
@exception = e
|
49
|
+
|
50
|
+
raise
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
alias ~ value
|
55
|
+
|
56
|
+
# Do the same as {#value}, but return nil in case of exception.
|
57
|
+
def value!
|
58
|
+
begin
|
59
|
+
value
|
60
|
+
rescue Exception
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
alias ! value!
|
66
|
+
end
|
67
|
+
|
68
|
+
module Kernel
|
69
|
+
# Helper to create a Thread::Delay
|
70
|
+
def delay (&block)
|
71
|
+
Thread::Delay.new(&block)
|
72
|
+
end
|
73
|
+
end
|
data/lib/thread/future.rb
CHANGED
@@ -10,12 +10,98 @@
|
|
10
10
|
|
11
11
|
require 'thread/promise'
|
12
12
|
|
13
|
-
|
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
|
14
17
|
def initialize (&block)
|
15
18
|
Thread.new {
|
16
|
-
|
19
|
+
begin
|
20
|
+
deliver block.call
|
21
|
+
rescue Exception => e
|
22
|
+
@exception = e
|
23
|
+
|
24
|
+
deliver nil
|
25
|
+
end
|
17
26
|
}
|
18
27
|
end
|
28
|
+
|
29
|
+
# Check if an exception has been raised.
|
30
|
+
def exception?
|
31
|
+
instance_variable_defined? :@exception
|
32
|
+
end
|
33
|
+
|
34
|
+
# Return the raised exception.
|
35
|
+
def exception
|
36
|
+
@exception
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check if the future has been called.
|
40
|
+
def delivered?
|
41
|
+
instance_variable_defined? :@value
|
42
|
+
end
|
43
|
+
|
44
|
+
alias realized? delivered?
|
45
|
+
|
46
|
+
# Get the value of the future, if it's not finished running this call will block.
|
47
|
+
#
|
48
|
+
# In case the block raises an exception, it will be raised, the exception is cached
|
49
|
+
# and will be raised every time you access the value.
|
50
|
+
def value
|
51
|
+
raise @exception if exception?
|
52
|
+
|
53
|
+
return @value if delivered?
|
54
|
+
|
55
|
+
mutex.synchronize {
|
56
|
+
cond.wait(mutex)
|
57
|
+
}
|
58
|
+
|
59
|
+
if exception?
|
60
|
+
raise @exception
|
61
|
+
else
|
62
|
+
@value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
alias ~ value
|
67
|
+
|
68
|
+
# Do the same as {#value}, but return nil in case of exception.
|
69
|
+
def value!
|
70
|
+
begin
|
71
|
+
value
|
72
|
+
rescue Exception
|
73
|
+
nil
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
alias ! value!
|
78
|
+
|
79
|
+
private
|
80
|
+
def deliver (value)
|
81
|
+
return if delivered?
|
82
|
+
|
83
|
+
@value = value
|
84
|
+
|
85
|
+
if cond?
|
86
|
+
mutex.synchronize {
|
87
|
+
cond.broadcast
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def cond?
|
95
|
+
instance_variable_defined? :@cond
|
96
|
+
end
|
97
|
+
|
98
|
+
def cond
|
99
|
+
@cond ||= ConditionVariable.new
|
100
|
+
end
|
101
|
+
|
102
|
+
def mutex
|
103
|
+
@mutex ||= Mutex.new
|
104
|
+
end
|
19
105
|
end
|
20
106
|
|
21
107
|
module Kernel
|
data/lib/thread/pool.rb
CHANGED
@@ -10,17 +10,27 @@
|
|
10
10
|
|
11
11
|
require 'thread'
|
12
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.
|
13
18
|
class Thread::Pool
|
19
|
+
# A task incapsulates a block being ran by the pool and the arguments to pass
|
20
|
+
# to it.
|
14
21
|
class Task
|
15
22
|
Timeout = Class.new(Exception)
|
16
23
|
Asked = Class.new(Exception)
|
17
24
|
|
18
25
|
attr_reader :pool, :timeout, :exception, :thread, :started_at
|
19
26
|
|
27
|
+
# Create a task in the given pool which will pass the arguments to the
|
28
|
+
# block.
|
20
29
|
def initialize (pool, *args, &block)
|
21
|
-
@pool
|
22
|
-
@arguments
|
23
|
-
@block
|
30
|
+
@pool = pool
|
31
|
+
@arguments = args
|
32
|
+
@block = block
|
33
|
+
|
24
34
|
@running = false
|
25
35
|
@finished = false
|
26
36
|
@timedout = false
|
@@ -32,6 +42,7 @@ class Thread::Pool
|
|
32
42
|
def timeout?; @timedout; end
|
33
43
|
def terminated?; @terminated; end
|
34
44
|
|
45
|
+
# Execute the task in the given thread.
|
35
46
|
def execute (thread)
|
36
47
|
return if terminated? || running? || finished?
|
37
48
|
|
@@ -39,7 +50,7 @@ class Thread::Pool
|
|
39
50
|
@running = true
|
40
51
|
@started_at = Time.now
|
41
52
|
|
42
|
-
pool.wake_up_timeout
|
53
|
+
pool.__send__.wake_up_timeout
|
43
54
|
|
44
55
|
begin
|
45
56
|
@block.call(*@arguments)
|
@@ -58,6 +69,7 @@ class Thread::Pool
|
|
58
69
|
@thread = nil
|
59
70
|
end
|
60
71
|
|
72
|
+
# Terminate the exception with an optionally given exception.
|
61
73
|
def terminate! (exception = Asked)
|
62
74
|
return if terminated? || finished? || timeout?
|
63
75
|
|
@@ -68,10 +80,12 @@ class Thread::Pool
|
|
68
80
|
@thread.raise exception
|
69
81
|
end
|
70
82
|
|
83
|
+
# Force the task to timeout.
|
71
84
|
def timeout!
|
72
85
|
terminate! Timeout
|
73
86
|
end
|
74
87
|
|
88
|
+
# Timeout the task after the given time.
|
75
89
|
def timeout_after (time)
|
76
90
|
@timeout = time
|
77
91
|
|
@@ -83,6 +97,13 @@ class Thread::Pool
|
|
83
97
|
|
84
98
|
attr_reader :min, :max, :spawned
|
85
99
|
|
100
|
+
# Create the pool with minimum and maximum threads.
|
101
|
+
#
|
102
|
+
# The pool will start with the minimum amount of threads created and will
|
103
|
+
# spawn new threads until the max is reached in case of need.
|
104
|
+
#
|
105
|
+
# A default block can be passed, which will be used to {#process} the passed
|
106
|
+
# data.
|
86
107
|
def initialize (min, max = nil, &block)
|
87
108
|
@min = min
|
88
109
|
@max = max || min
|
@@ -108,12 +129,26 @@ class Thread::Pool
|
|
108
129
|
}
|
109
130
|
end
|
110
131
|
|
132
|
+
# Check if the pool has been shut down.
|
111
133
|
def shutdown?; !!@shutdown; end
|
112
134
|
|
113
|
-
|
114
|
-
def auto_trim
|
115
|
-
|
135
|
+
# Check if auto trimming is enabled.
|
136
|
+
def auto_trim?
|
137
|
+
@auto_trim
|
138
|
+
end
|
139
|
+
|
140
|
+
# Enable auto trimming, unneeded threads will be deleted until the minimum
|
141
|
+
# is reached.
|
142
|
+
def auto_trim!
|
143
|
+
@auto_trim = true
|
144
|
+
end
|
116
145
|
|
146
|
+
# Disable auto trimming.
|
147
|
+
def no_auto_trim!
|
148
|
+
@auto_trim = false
|
149
|
+
end
|
150
|
+
|
151
|
+
# Resize the pool with the passed arguments.
|
117
152
|
def resize (min, max = nil)
|
118
153
|
@min = min
|
119
154
|
@max = max || min
|
@@ -121,12 +156,18 @@ class Thread::Pool
|
|
121
156
|
trim!
|
122
157
|
end
|
123
158
|
|
159
|
+
# Get the amount of tasks that still have to be run.
|
124
160
|
def backlog
|
125
161
|
@mutex.synchronize {
|
126
162
|
@todo.length
|
127
163
|
}
|
128
164
|
end
|
129
165
|
|
166
|
+
# Add a task to the pool which will execute the block with the given
|
167
|
+
# argument.
|
168
|
+
#
|
169
|
+
# If no block is passed the default block will be used if present, an
|
170
|
+
# ArgumentError will be raised otherwise.
|
130
171
|
def process (*args, &block)
|
131
172
|
unless block || @block
|
132
173
|
raise ArgumentError, 'you must pass a block'
|
@@ -151,6 +192,8 @@ class Thread::Pool
|
|
151
192
|
|
152
193
|
alias << process
|
153
194
|
|
195
|
+
# Trim the unused threads, if forced threads will be trimmed even if there
|
196
|
+
# are tasks waiting.
|
154
197
|
def trim (force = false)
|
155
198
|
@mutex.synchronize {
|
156
199
|
if (force || @waiting > 0) && @spawned - @trim_requests > @min
|
@@ -162,10 +205,12 @@ class Thread::Pool
|
|
162
205
|
self
|
163
206
|
end
|
164
207
|
|
208
|
+
# Force #{trim}.
|
165
209
|
def trim!
|
166
210
|
trim true
|
167
211
|
end
|
168
212
|
|
213
|
+
# Shut down the pool instantly without finishing to execute tasks.
|
169
214
|
def shutdown!
|
170
215
|
@mutex.synchronize {
|
171
216
|
@shutdown = :now
|
@@ -177,6 +222,7 @@ class Thread::Pool
|
|
177
222
|
self
|
178
223
|
end
|
179
224
|
|
225
|
+
# Shut down the pool, it will block until all tasks have finished running.
|
180
226
|
def shutdown
|
181
227
|
@mutex.synchronize {
|
182
228
|
@shutdown = :nicely
|
@@ -196,12 +242,14 @@ class Thread::Pool
|
|
196
242
|
self
|
197
243
|
end
|
198
244
|
|
245
|
+
# Join on all threads in the pool.
|
199
246
|
def join
|
200
247
|
@workers.first.join until @workers.empty?
|
201
248
|
|
202
249
|
self
|
203
250
|
end
|
204
251
|
|
252
|
+
# Define a timeout for a task.
|
205
253
|
def timeout_for (task, timeout)
|
206
254
|
unless @timeout
|
207
255
|
spawn_timeout_thread
|
@@ -214,6 +262,7 @@ class Thread::Pool
|
|
214
262
|
}
|
215
263
|
end
|
216
264
|
|
265
|
+
# Shutdown the pool after a given amount of time.
|
217
266
|
def shutdown_after (timeout)
|
218
267
|
Thread.new {
|
219
268
|
sleep timeout
|
@@ -224,13 +273,13 @@ class Thread::Pool
|
|
224
273
|
self
|
225
274
|
end
|
226
275
|
|
276
|
+
private
|
227
277
|
def wake_up_timeout
|
228
278
|
if defined? @pipes
|
229
279
|
@pipes.last.write_nonblock 'x' rescue nil
|
230
280
|
end
|
231
281
|
end
|
232
282
|
|
233
|
-
private
|
234
283
|
def spawn_thread
|
235
284
|
@spawned += 1
|
236
285
|
|
@@ -281,7 +330,7 @@ private
|
|
281
330
|
@timeout = Thread.new {
|
282
331
|
loop do
|
283
332
|
now = Time.now
|
284
|
-
timeout = @timeouts.map {|task,
|
333
|
+
timeout = @timeouts.map {|task, time|
|
285
334
|
next unless task.started_at
|
286
335
|
|
287
336
|
now - task.started_at + task.timeout
|
data/lib/thread/promise.rb
CHANGED
@@ -10,11 +10,16 @@
|
|
10
10
|
|
11
11
|
require 'thread'
|
12
12
|
|
13
|
+
# A promise is an object that lets you wait for a value to be delivered to it.
|
13
14
|
class Thread::Promise
|
15
|
+
# Check if a value has been delivered.
|
14
16
|
def delivered?
|
15
17
|
instance_variable_defined? :@value
|
16
18
|
end
|
17
19
|
|
20
|
+
alias realized? delivered?
|
21
|
+
|
22
|
+
# Deliver a value.
|
18
23
|
def deliver (value)
|
19
24
|
return if delivered?
|
20
25
|
|
@@ -31,6 +36,8 @@ class Thread::Promise
|
|
31
36
|
|
32
37
|
alias << deliver
|
33
38
|
|
39
|
+
# Get the value that's been delivered, if none has been delivered yet the call
|
40
|
+
# will block until one is delivered.
|
34
41
|
def value
|
35
42
|
return @value if delivered?
|
36
43
|
|
@@ -38,7 +45,7 @@ class Thread::Promise
|
|
38
45
|
cond.wait(mutex)
|
39
46
|
}
|
40
47
|
|
41
|
-
|
48
|
+
@value
|
42
49
|
end
|
43
50
|
|
44
51
|
alias ~ value
|
@@ -10,6 +10,10 @@
|
|
10
10
|
|
11
11
|
require 'thread'
|
12
12
|
|
13
|
+
# A recursive mutex lets you lock in various threads recursively, allowing
|
14
|
+
# you to do multiple locks one inside another.
|
15
|
+
#
|
16
|
+
# You really shouldn't use this, but in some cases it makes your life easier.
|
13
17
|
class RecursiveMutex < Mutex
|
14
18
|
def initialize
|
15
19
|
@threads = Hash.new { |h, k| h[k] = 0 }
|
@@ -17,6 +21,7 @@ class RecursiveMutex < Mutex
|
|
17
21
|
super
|
18
22
|
end
|
19
23
|
|
24
|
+
# Lock the mutex.
|
20
25
|
def lock
|
21
26
|
@thread[Thread.current] += 1
|
22
27
|
|
@@ -25,6 +30,7 @@ class RecursiveMutex < Mutex
|
|
25
30
|
end
|
26
31
|
end
|
27
32
|
|
33
|
+
# Unlock the mutex.
|
28
34
|
def unlock
|
29
35
|
@thread[Thread.current] -= 1
|
30
36
|
|
data/thread.gemspec
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
Gem::Specification.new {|s|
|
2
2
|
s.name = 'thread'
|
3
|
-
s.version = '0.0.
|
3
|
+
s.version = '0.0.3'
|
4
4
|
s.author = 'meh.'
|
5
5
|
s.email = 'meh@schizofreni.co'
|
6
6
|
s.homepage = 'http://github.com/meh/ruby-thread'
|
7
7
|
s.platform = Gem::Platform::RUBY
|
8
|
-
s.summary = 'Various extensions to the base thread
|
9
|
-
s.description = 'Includes a thread pool, message passing capabilities, a recursive mutex, promise and
|
8
|
+
s.summary = 'Various extensions to the base thread library.'
|
9
|
+
s.description = 'Includes a thread pool, message passing capabilities, a recursive mutex, promise, future and delay.'
|
10
10
|
|
11
11
|
s.files = `git ls-files`.split("\n")
|
12
12
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: thread
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -12,7 +12,7 @@ cert_chain: []
|
|
12
12
|
date: 2013-02-25 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Includes a thread pool, message passing capabilities, a recursive mutex,
|
15
|
-
promise and
|
15
|
+
promise, future and delay.
|
16
16
|
email: meh@schizofreni.co
|
17
17
|
executables: []
|
18
18
|
extensions: []
|
@@ -20,6 +20,7 @@ extra_rdoc_files: []
|
|
20
20
|
files:
|
21
21
|
- README.md
|
22
22
|
- lib/thread/channel.rb
|
23
|
+
- lib/thread/delay.rb
|
23
24
|
- lib/thread/future.rb
|
24
25
|
- lib/thread/pool.rb
|
25
26
|
- lib/thread/promise.rb
|
@@ -48,5 +49,6 @@ rubyforge_project:
|
|
48
49
|
rubygems_version: 1.8.23
|
49
50
|
signing_key:
|
50
51
|
specification_version: 3
|
51
|
-
summary: Various extensions to the base thread
|
52
|
+
summary: Various extensions to the base thread library.
|
52
53
|
test_files: []
|
54
|
+
has_rdoc:
|