thread 0.0.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.
- data/README.md +68 -0
- data/lib/thread/channel.rb +74 -0
- data/lib/thread/pool.rb +309 -0
- data/lib/thread/recursive_mutex.rb +37 -0
- data/thread.gemspec +14 -0
- metadata +49 -0
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
thread - various extensions to the thread stdlib
|
2
|
+
================================================
|
3
|
+
|
4
|
+
Pool
|
5
|
+
====
|
6
|
+
All the implementations I looked at were either buggy or wasted CPU resources
|
7
|
+
for no apparent reason, for example used a sleep of 0.01 seconds to then check for
|
8
|
+
readiness and stuff like this.
|
9
|
+
|
10
|
+
This implementation uses standard locking functions to work properly across multiple Ruby
|
11
|
+
implementations.
|
12
|
+
|
13
|
+
Example
|
14
|
+
-------
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
require 'thread/pool'
|
18
|
+
|
19
|
+
pool = Thread::Pool.new(4)
|
20
|
+
|
21
|
+
10.times {
|
22
|
+
pool.process {
|
23
|
+
sleep 2
|
24
|
+
|
25
|
+
puts 'lol'
|
26
|
+
}
|
27
|
+
}
|
28
|
+
|
29
|
+
pool.shutdown
|
30
|
+
```
|
31
|
+
|
32
|
+
You should get 4 lols every 2 seconds and it should exit after 10 of them.
|
33
|
+
|
34
|
+
Channel
|
35
|
+
=======
|
36
|
+
This implements a channel where you can write messages and receive messages.
|
37
|
+
|
38
|
+
Example
|
39
|
+
-------
|
40
|
+
|
41
|
+
```ruby
|
42
|
+
require 'thread/channel'
|
43
|
+
|
44
|
+
channel = Thread::Channel.new
|
45
|
+
channel.send 'wat'
|
46
|
+
channel.receive # => 'wat'
|
47
|
+
|
48
|
+
channel = Thread::Channel.new { |o| o.is_a?(Integer) }
|
49
|
+
channel.send 'wat' # => ArgumentError: guard mismatch
|
50
|
+
|
51
|
+
Thread.new {
|
52
|
+
while num = channel.receive(&:even?)
|
53
|
+
puts 'Aye!'
|
54
|
+
end
|
55
|
+
}
|
56
|
+
|
57
|
+
Thread.new {
|
58
|
+
while num = channel.receive(&:odd?)
|
59
|
+
puts 'Arrr!'
|
60
|
+
end
|
61
|
+
}
|
62
|
+
|
63
|
+
loop {
|
64
|
+
channel.send rand(1_000_000_000)
|
65
|
+
|
66
|
+
sleep 0.5
|
67
|
+
}
|
68
|
+
```
|
@@ -0,0 +1,74 @@
|
|
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
|
+
class Thread::Channel
|
14
|
+
def initialize (messages = [], &block)
|
15
|
+
@messages = []
|
16
|
+
@mutex = Mutex.new
|
17
|
+
@cond = ConditionVariable.new
|
18
|
+
@check = block
|
19
|
+
|
20
|
+
messages.each {|o|
|
21
|
+
send o
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def send (what)
|
26
|
+
if @check && !@check.call(what)
|
27
|
+
raise ArgumentError, 'guard mismatch'
|
28
|
+
end
|
29
|
+
|
30
|
+
@mutex.synchronize {
|
31
|
+
@messages << what
|
32
|
+
@cond.broadcast
|
33
|
+
}
|
34
|
+
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive (&block)
|
39
|
+
message = nil
|
40
|
+
|
41
|
+
if block
|
42
|
+
found = false
|
43
|
+
|
44
|
+
until found
|
45
|
+
@mutex.synchronize {
|
46
|
+
if index = @messages.find_index(&block)
|
47
|
+
message = @messages.delete_at(index)
|
48
|
+
found = true
|
49
|
+
else
|
50
|
+
@cond.wait @mutex
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
else
|
55
|
+
@mutex.synchronize {
|
56
|
+
if @messages.empty?
|
57
|
+
@cond.wait @mutex
|
58
|
+
end
|
59
|
+
|
60
|
+
message = @messages.shift
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
message
|
65
|
+
end
|
66
|
+
|
67
|
+
def receive! (&block)
|
68
|
+
if block
|
69
|
+
@messages.delete_at(@messages.find_index(&block))
|
70
|
+
else
|
71
|
+
@messages.shift
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/thread/pool.rb
ADDED
@@ -0,0 +1,309 @@
|
|
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
|
+
class Thread::Pool
|
14
|
+
class Task
|
15
|
+
Timeout = Class.new(Exception)
|
16
|
+
Asked = Class.new(Exception)
|
17
|
+
|
18
|
+
attr_reader :pool, :timeout, :exception, :thread, :started_at
|
19
|
+
|
20
|
+
def initialize (pool, *args, &block)
|
21
|
+
@pool = pool
|
22
|
+
@arguments = args
|
23
|
+
@block = block
|
24
|
+
end
|
25
|
+
|
26
|
+
def running?; @running; end
|
27
|
+
def finished?; @finished; end
|
28
|
+
def timeout?; @timedout; end
|
29
|
+
def terminated?; @terminated; end
|
30
|
+
|
31
|
+
def execute (thread)
|
32
|
+
return if terminated? || running? || finished?
|
33
|
+
|
34
|
+
@thread = thread
|
35
|
+
@running = true
|
36
|
+
@started_at = Time.now
|
37
|
+
|
38
|
+
pool.wake_up_timeout
|
39
|
+
|
40
|
+
begin
|
41
|
+
@block.call(*@arguments)
|
42
|
+
rescue Exception => e
|
43
|
+
if e.is_a? Timeout
|
44
|
+
@timedout = true
|
45
|
+
elsif e.is_a? Asked
|
46
|
+
return
|
47
|
+
else
|
48
|
+
@exception = reason
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
@running = false
|
53
|
+
@finished = true
|
54
|
+
@thread = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def terminate! (exception = Asked)
|
58
|
+
return if terminated? || finished? || timeout?
|
59
|
+
|
60
|
+
@terminated = true
|
61
|
+
|
62
|
+
return unless running?
|
63
|
+
|
64
|
+
@thread.raise exception
|
65
|
+
end
|
66
|
+
|
67
|
+
def timeout!
|
68
|
+
terminate! Timeout
|
69
|
+
end
|
70
|
+
|
71
|
+
def timeout_after (time)
|
72
|
+
@timeout = time
|
73
|
+
|
74
|
+
pool.timeout_for self, time
|
75
|
+
|
76
|
+
self
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_reader :min, :max, :spawned
|
81
|
+
|
82
|
+
def initialize (min, max = nil, &block)
|
83
|
+
@min = min
|
84
|
+
@max = max || min
|
85
|
+
@block = block
|
86
|
+
|
87
|
+
@cond = ConditionVariable.new
|
88
|
+
@mutex = Mutex.new
|
89
|
+
|
90
|
+
@todo = []
|
91
|
+
@workers = []
|
92
|
+
@timeouts = {}
|
93
|
+
|
94
|
+
@spawned = 0
|
95
|
+
@waiting = 0
|
96
|
+
@shutdown = false
|
97
|
+
@trim_requests = 0
|
98
|
+
@auto_trim = false
|
99
|
+
|
100
|
+
@mutex.synchronize {
|
101
|
+
min.times {
|
102
|
+
spawn_thread
|
103
|
+
}
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
def shutdown?; !!@shutdown; end
|
108
|
+
|
109
|
+
def auto_trim?; @auto_trim; end
|
110
|
+
def auto_trim!; @auto_trim = true; end
|
111
|
+
def no_auto_trim!; @auto_trim = false; end
|
112
|
+
|
113
|
+
def resize (min, max = nil)
|
114
|
+
@min = min
|
115
|
+
@max = max || min
|
116
|
+
|
117
|
+
trim!
|
118
|
+
end
|
119
|
+
|
120
|
+
def backlog
|
121
|
+
@mutex.synchronize {
|
122
|
+
@todo.length
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
def process (*args, &block)
|
127
|
+
unless block || @block
|
128
|
+
raise ArgumentError, 'you must pass a block'
|
129
|
+
end
|
130
|
+
|
131
|
+
task = Task.new(self, *args, &(block || @block))
|
132
|
+
|
133
|
+
@mutex.synchronize {
|
134
|
+
raise 'unable to add work while shutting down' if shutdown?
|
135
|
+
|
136
|
+
@todo << task
|
137
|
+
|
138
|
+
if @waiting == 0 && @spawned < @max
|
139
|
+
spawn_thread
|
140
|
+
end
|
141
|
+
|
142
|
+
@cond.signal
|
143
|
+
}
|
144
|
+
|
145
|
+
task
|
146
|
+
end
|
147
|
+
|
148
|
+
alias << process
|
149
|
+
|
150
|
+
def trim (force = false)
|
151
|
+
@mutex.synchronize {
|
152
|
+
if (force || @waiting > 0) && @spawned - @trim_requests > @min
|
153
|
+
@trim_requests -= 1
|
154
|
+
@cond.signal
|
155
|
+
end
|
156
|
+
}
|
157
|
+
|
158
|
+
self
|
159
|
+
end
|
160
|
+
|
161
|
+
def trim!
|
162
|
+
trim true
|
163
|
+
end
|
164
|
+
|
165
|
+
def shutdown!
|
166
|
+
@mutex.synchronize {
|
167
|
+
@shutdown = :now
|
168
|
+
@cond.broadcast
|
169
|
+
}
|
170
|
+
|
171
|
+
wake_up_timeout
|
172
|
+
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
def shutdown
|
177
|
+
@mutex.synchronize {
|
178
|
+
@shutdown = :nicely
|
179
|
+
@cond.broadcast
|
180
|
+
}
|
181
|
+
|
182
|
+
join
|
183
|
+
|
184
|
+
if @timeout
|
185
|
+
@shutdown = :now
|
186
|
+
|
187
|
+
wake_up_timeout
|
188
|
+
|
189
|
+
@timeout.join
|
190
|
+
end
|
191
|
+
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
def join
|
196
|
+
@workers.first.join until @workers.empty?
|
197
|
+
|
198
|
+
self
|
199
|
+
end
|
200
|
+
|
201
|
+
def timeout_for (task, timeout)
|
202
|
+
unless @timeout
|
203
|
+
spawn_timeout_thread
|
204
|
+
end
|
205
|
+
|
206
|
+
@mutex.synchronize {
|
207
|
+
@timeouts[task] = timeout
|
208
|
+
|
209
|
+
wake_up_timeout
|
210
|
+
}
|
211
|
+
end
|
212
|
+
|
213
|
+
def shutdown_after (timeout)
|
214
|
+
Thread.new {
|
215
|
+
sleep timeout
|
216
|
+
|
217
|
+
shutdown
|
218
|
+
}
|
219
|
+
|
220
|
+
self
|
221
|
+
end
|
222
|
+
|
223
|
+
def wake_up_timeout
|
224
|
+
if @pipes
|
225
|
+
@pipes.last.write_nonblock 'x' rescue nil
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
def spawn_thread
|
231
|
+
@spawned += 1
|
232
|
+
|
233
|
+
thread = Thread.new {
|
234
|
+
loop do
|
235
|
+
task = @mutex.synchronize {
|
236
|
+
if @todo.empty?
|
237
|
+
while @todo.empty?
|
238
|
+
if @trim_requests > 0
|
239
|
+
@trim_requests -= 1
|
240
|
+
|
241
|
+
break
|
242
|
+
end
|
243
|
+
|
244
|
+
break if shutdown?
|
245
|
+
|
246
|
+
@waiting += 1
|
247
|
+
@cond.wait @mutex
|
248
|
+
@waiting -= 1
|
249
|
+
|
250
|
+
break !shutdown?
|
251
|
+
end or break
|
252
|
+
end
|
253
|
+
|
254
|
+
@todo.shift
|
255
|
+
} or break
|
256
|
+
|
257
|
+
task.execute(thread)
|
258
|
+
|
259
|
+
break if @shutdown == :now
|
260
|
+
|
261
|
+
trim if auto_trim? && @spawned > @min
|
262
|
+
end
|
263
|
+
|
264
|
+
@mutex.synchronize {
|
265
|
+
@spawned -= 1
|
266
|
+
@workers.delete thread
|
267
|
+
}
|
268
|
+
}
|
269
|
+
|
270
|
+
@workers << thread
|
271
|
+
|
272
|
+
thread
|
273
|
+
end
|
274
|
+
|
275
|
+
def spawn_timeout_thread
|
276
|
+
@pipes = IO.pipe
|
277
|
+
@timeout = Thread.new {
|
278
|
+
loop do
|
279
|
+
now = Time.now
|
280
|
+
timeout = @timeouts.map {|task, timeout|
|
281
|
+
next unless task.started_at
|
282
|
+
|
283
|
+
now - task.started_at + task.timeout
|
284
|
+
}.compact.min unless @timeouts.empty?
|
285
|
+
|
286
|
+
readable, = IO.select([@pipes.first], nil, nil, timeout)
|
287
|
+
|
288
|
+
break if @shutdown == :now
|
289
|
+
|
290
|
+
if readable && !readable.empty?
|
291
|
+
readable.first.read_nonblock 1024
|
292
|
+
end
|
293
|
+
|
294
|
+
now = Time.now
|
295
|
+
@timeouts.each {|task, time|
|
296
|
+
next if !task.started_at || task.terminated? || task.finished?
|
297
|
+
|
298
|
+
if now > task.started_at + task.timeout
|
299
|
+
task.timeout!
|
300
|
+
end
|
301
|
+
}
|
302
|
+
|
303
|
+
@timeouts.reject! { |task, _| task.terminated? || task.finished? }
|
304
|
+
|
305
|
+
break if @shutdown == :now
|
306
|
+
end
|
307
|
+
}
|
308
|
+
end
|
309
|
+
end
|
@@ -0,0 +1,37 @@
|
|
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
|
+
class RecursiveMutex < Mutex
|
14
|
+
def initialize
|
15
|
+
@threads = Hash.new { |h, k| h[k] = 0 }
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def lock
|
21
|
+
@thread[Thread.current] += 1
|
22
|
+
|
23
|
+
if @thread[Thread.current] == 1
|
24
|
+
super
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def unlock
|
29
|
+
@thread[Thread.current] -= 1
|
30
|
+
|
31
|
+
if @thread[Thread.current] == 0
|
32
|
+
@thread.delete(Thread.current)
|
33
|
+
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/thread.gemspec
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Gem::Specification.new {|s|
|
2
|
+
s.name = 'thread'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.author = 'meh.'
|
5
|
+
s.email = 'meh@schizofreni.co'
|
6
|
+
s.homepage = 'http://github.com/meh/ruby-thread'
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.summary = 'Various extensions to the base thread stdlib.'
|
9
|
+
|
10
|
+
s.files = `git ls-files`.split("\n")
|
11
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
12
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
13
|
+
s.require_paths = ['lib']
|
14
|
+
}
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: thread
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- meh.
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-10-22 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description:
|
15
|
+
email: meh@schizofreni.co
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/thread/channel.rb
|
22
|
+
- lib/thread/pool.rb
|
23
|
+
- lib/thread/recursive_mutex.rb
|
24
|
+
- thread.gemspec
|
25
|
+
homepage: http://github.com/meh/ruby-thread
|
26
|
+
licenses: []
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
none: false
|
33
|
+
requirements:
|
34
|
+
- - ! '>='
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ! '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0'
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 1.8.24
|
46
|
+
signing_key:
|
47
|
+
specification_version: 3
|
48
|
+
summary: Various extensions to the base thread stdlib.
|
49
|
+
test_files: []
|