pipa-threadpool 0.2.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/MIT-LICENSE +20 -0
- data/README.rdoc +29 -0
- data/Rakefile +27 -0
- data/lib/pipa-threadpool.rb +1 -0
- data/lib/threadpool.rb +189 -0
- data/test/threadpool_test.rb +18 -0
- metadata +60 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Igor Gunko
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
=== Introduction
|
2
|
+
O HAI! My name is Pool, Thread Pool. Now in Ruby.
|
3
|
+
|
4
|
+
=== Installation
|
5
|
+
gem install pipa-threadpool -s gems.github.com
|
6
|
+
|
7
|
+
=== Usage
|
8
|
+
pool = ThreadPool.new
|
9
|
+
...
|
10
|
+
pool.run(...) do |...|
|
11
|
+
...
|
12
|
+
end
|
13
|
+
...
|
14
|
+
pool.close
|
15
|
+
|
16
|
+
=== More info in docs
|
17
|
+
http://rdoc.info/projects/pipa/threadpool
|
18
|
+
|
19
|
+
=== Bugs & such
|
20
|
+
Please report via Github issue tracking.
|
21
|
+
|
22
|
+
=== See also
|
23
|
+
http://github.com/pipa/monkeyjob -- Background job runner
|
24
|
+
http://github.com/pipa/xmlnuts -- Ruby <-> XML mapping
|
25
|
+
http://github.com/pipa/statelogic -- A simple state machine for ActiveRecord
|
26
|
+
|
27
|
+
|
28
|
+
Free hint: If you liek mudkipz^W^Wfeel like generous today you can tip me at http://tipjoy.com/u/pisuka
|
29
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
$KCODE = 'u'
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/clean'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/testtask'
|
9
|
+
|
10
|
+
Rake::GemPackageTask.new(Gem::Specification.load('threadpool.gemspec')) do |p|
|
11
|
+
p.need_tar = true
|
12
|
+
p.need_zip = true
|
13
|
+
end
|
14
|
+
|
15
|
+
Rake::RDocTask.new do |rdoc|
|
16
|
+
files =['README.rdoc', 'MIT-LICENSE', 'lib/**/*.rb']
|
17
|
+
rdoc.rdoc_files.add(files)
|
18
|
+
rdoc.main = "README.rdoc" # page to start on
|
19
|
+
rdoc.title = "XmlNuts Documentation"
|
20
|
+
rdoc.rdoc_dir = 'doc/rdoc' # rdoc output folder
|
21
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
22
|
+
end
|
23
|
+
|
24
|
+
Rake::TestTask.new do |t|
|
25
|
+
t.test_files = FileList['test/**/*.rb']
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'threadpool'
|
data/lib/threadpool.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'thwait'
|
3
|
+
require 'monitor'
|
4
|
+
|
5
|
+
# This is unsurprisingly a thread pool.
|
6
|
+
# It can run your jobs asynchronously.
|
7
|
+
# It can grow and shrink depending on the load.
|
8
|
+
# Like any good pool it can be... closed!
|
9
|
+
class ThreadPool
|
10
|
+
DEFAULT_CORE_WORKERS = 4
|
11
|
+
DEFAULT_KEEP_ALIVE_TIME = 5
|
12
|
+
|
13
|
+
class Job #:nodoc:
|
14
|
+
def initialize(*args, &handler)
|
15
|
+
@args, @handler = args, handler
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
@handler.call(*@args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
@@controllers = ThreadGroup.new
|
24
|
+
|
25
|
+
# new([[core_workers[, max_workers[, keep_alive_time]],] options]) [{|pool| ... }]
|
26
|
+
#
|
27
|
+
# === Arguments
|
28
|
+
# [+core_workers+] Number of core worker threads. The pool will never shrink below this point.
|
29
|
+
# [+max_workers+] Maximum number of worker threads allowed per this pool.
|
30
|
+
# The pool will never expand over this limit.
|
31
|
+
# Default is +core_workers * 2+
|
32
|
+
# [+keep_alive_time+] Time to keep non-core workers alive. Default is 5 sec.
|
33
|
+
# [+options+] +:core =>+ _core_workers_,
|
34
|
+
# +:max =>+ _max_workers_,
|
35
|
+
# +:keep_alive =>+ _keep_alive_time_,
|
36
|
+
# +:init_core => false+ to defer initial setup of core workers.
|
37
|
+
#
|
38
|
+
# When called with a block the pool will be closed upon exit from the block.
|
39
|
+
# Graceful +close+ will be used, a non-bang version.
|
40
|
+
#
|
41
|
+
# === Example:
|
42
|
+
# ThreadPool.new 10, 25, 6.7, :init_core => false do |pool|
|
43
|
+
# ...
|
44
|
+
# end
|
45
|
+
def initialize(*args)
|
46
|
+
extend MonitorMixin
|
47
|
+
|
48
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
49
|
+
|
50
|
+
@core_workers = (args[0] || options[:core] || DEFAULT_CORE_WORKERS).to_i
|
51
|
+
raise ArgumentError, "core_workers must be a positive integer" if @core_workers <= 0
|
52
|
+
|
53
|
+
@max_workers = (args[1] || options[:max] || @core_workers * 2).to_i
|
54
|
+
raise ArgumentError, "max_workers must be >= core_workers" if @max_workers < @core_workers
|
55
|
+
|
56
|
+
@keep_alive_time = (args[2] || options[:keep_alive] || DEFAULT_KEEP_ALIVE_TIME).to_f
|
57
|
+
raise ArgumentError, "keep_alive_time must be a non-negative real number" if @keep_alive_time < 0
|
58
|
+
|
59
|
+
@workers, @jobs = ThreadGroup.new, Queue.new
|
60
|
+
|
61
|
+
@worker_routine = proc do
|
62
|
+
while job = @jobs.pop
|
63
|
+
job.run rescue nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
@controller = Thread.new do
|
68
|
+
loop do
|
69
|
+
sleep(@keep_alive_time)
|
70
|
+
break if @dead
|
71
|
+
synchronize do
|
72
|
+
n = @jobs.num_waiting - @core_workers
|
73
|
+
stop_workers([n / 2, 1].max) if n >= 0
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
@@controllers.add(@controller)
|
78
|
+
|
79
|
+
create_workers(@core_workers) if options.fetch(:init_core, true)
|
80
|
+
|
81
|
+
begin
|
82
|
+
yield self
|
83
|
+
ensure
|
84
|
+
shutdown
|
85
|
+
end if block_given?
|
86
|
+
end
|
87
|
+
|
88
|
+
# live? => boolean
|
89
|
+
#
|
90
|
+
# Pool is live when it's not dead.
|
91
|
+
# Pool is dead when it's closed.
|
92
|
+
def live?
|
93
|
+
synchronize { !@dead }
|
94
|
+
end
|
95
|
+
|
96
|
+
# run([arg1[, arg2[, ...]]]) {|[arg1[, arg2[, ...]]]| ... } -> pool
|
97
|
+
#
|
98
|
+
# Schedule the block to run asynchronously on a worker thread. Return immediately.
|
99
|
+
# Any arguments passed to this method will be passed to the block.
|
100
|
+
#
|
101
|
+
# When there are no idle workers the pool will grow.
|
102
|
+
# When max pool size is reached the job will be queued up until better times.
|
103
|
+
#
|
104
|
+
# === Example:
|
105
|
+
# pool.run('go to hell') do |greeting|
|
106
|
+
# puts greeting
|
107
|
+
# end
|
108
|
+
def run(*args, &block)
|
109
|
+
run_core(true, *args, &block)
|
110
|
+
end
|
111
|
+
|
112
|
+
# try_run([arg1[, arg2[, ...]]]) {|[arg1[, arg2[, ...]]]| ... } -> pool or nil
|
113
|
+
#
|
114
|
+
# Try to run the block asynchronously on a worker thread (see +run+).
|
115
|
+
# If there are no idle workers immediately available and the pool reached its maximum size,
|
116
|
+
# then do not enqueue the job and return +nil+.
|
117
|
+
#
|
118
|
+
# === Example:
|
119
|
+
# puts 'zomg' unless pool.try_run('go to hell') {|greeting| puts greeting }
|
120
|
+
def try_run(*args, &block)
|
121
|
+
run_core(false, *args, &block)
|
122
|
+
end
|
123
|
+
|
124
|
+
# close
|
125
|
+
#
|
126
|
+
# Rape me gently. Waits until all the jobs are done and destroys the pool.
|
127
|
+
def close
|
128
|
+
_sync do
|
129
|
+
@dead = true
|
130
|
+
@controller.run
|
131
|
+
stop_workers(@workers.list.size)
|
132
|
+
end
|
133
|
+
ThreadsWait.all_waits(@controller, *@workers.list)
|
134
|
+
self
|
135
|
+
end
|
136
|
+
|
137
|
+
# close!
|
138
|
+
#
|
139
|
+
# Rape me hard. Instantly kills the workers. Ensure blocks will be called though (last prayer on).
|
140
|
+
def close!
|
141
|
+
_sync do
|
142
|
+
@dead = true
|
143
|
+
@controller.run
|
144
|
+
@workers.list.each {|w| w.kill }
|
145
|
+
end
|
146
|
+
self
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
|
151
|
+
def run_core(enqueue, *args, &block) #:nodoc:
|
152
|
+
raise ArgumentError, 'block must be provided' unless block_given?
|
153
|
+
_sync do
|
154
|
+
if @jobs.num_waiting == 0
|
155
|
+
if @workers.list.size < @max_workers
|
156
|
+
create_worker
|
157
|
+
else
|
158
|
+
return nil unless enqueue
|
159
|
+
end
|
160
|
+
end
|
161
|
+
@jobs.push(Job.new(*args, &block))
|
162
|
+
end
|
163
|
+
self
|
164
|
+
end
|
165
|
+
|
166
|
+
def _sync #:nodoc:
|
167
|
+
synchronize do
|
168
|
+
check_state
|
169
|
+
yield
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def check_state #:nodoc:
|
174
|
+
raise "pool's closed" if @dead
|
175
|
+
end
|
176
|
+
|
177
|
+
def create_worker #:nodoc:
|
178
|
+
@workers.add(Thread.new(&@worker_routine))
|
179
|
+
end
|
180
|
+
|
181
|
+
def create_workers(n) #:nodoc:
|
182
|
+
n.times { create_worker }
|
183
|
+
end
|
184
|
+
|
185
|
+
def stop_workers(n) #:nodoc:
|
186
|
+
n.times { @jobs << nil }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'lib/threadpool'
|
3
|
+
|
4
|
+
|
5
|
+
class ThreadPoolTest < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_me
|
10
|
+
@pool = ThreadPool.new(2, 15, 1)
|
11
|
+
n = 0
|
12
|
+
p = proc {|x| n += x }
|
13
|
+
100.times {|i| @pool.run(i, &p) }
|
14
|
+
@pool.close
|
15
|
+
assert_equal 4950, n
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
metadata
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pipa-threadpool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Igor Gunko
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-06-06 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: tekmon@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
- MIT-LICENSE
|
25
|
+
files:
|
26
|
+
- README.rdoc
|
27
|
+
- MIT-LICENSE
|
28
|
+
- Rakefile
|
29
|
+
- lib/threadpool.rb
|
30
|
+
- lib/pipa-threadpool.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: http://github.com/pipa/threadpool
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options:
|
35
|
+
- --line-numbers
|
36
|
+
- --main
|
37
|
+
- README.rdoc
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: "0"
|
45
|
+
version:
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: "0"
|
51
|
+
version:
|
52
|
+
requirements: []
|
53
|
+
|
54
|
+
rubyforge_project:
|
55
|
+
rubygems_version: 1.2.0
|
56
|
+
signing_key:
|
57
|
+
specification_version: 2
|
58
|
+
summary: Thread pool for Ruby
|
59
|
+
test_files:
|
60
|
+
- test/threadpool_test.rb
|