threadpool 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/README.md +3 -7
  2. data/lib/threadpool.rb +105 -108
  3. data/threadpool.gemspec +2 -2
  4. metadata +4 -4
data/README.md CHANGED
@@ -4,11 +4,8 @@ All the implementations I looked at were either buggy or wasted CPU resources
4
4
  for no apparent reason, for example used a sleep of 0.01 seconds to then check for
5
5
  readiness and stuff like this.
6
6
 
7
- This implementation uses `IO.select` instead, there is no timed sleep, it just stays
8
- there waiting for input, which will then come from a `#wake_up` call that writes on a pipe.
9
-
10
- `IO.select` should be present everywhere so this should be cross-platform and doesn't waste
11
- CPU resources. Keep in mind that each worker uses 2 file descriptors (reading and writing pipe).
7
+ This implementation uses standard locking functions to work properly across multiple Ruby
8
+ implementations.
12
9
 
13
10
  Example
14
11
  -------
@@ -16,8 +13,7 @@ Example
16
13
  ```ruby
17
14
  require 'threadpool'
18
15
 
19
- pool = ThreadPool.new
20
- pool.resize(4)
16
+ pool = ThreadPool.new(4)
21
17
 
22
18
  0.upto(10) { pool.process { sleep 2; puts 'lol' } }
23
19
 
data/lib/threadpool.rb CHANGED
@@ -8,146 +8,143 @@
8
8
  # 0. You just DO WHAT THE FUCK YOU WANT TO.
9
9
  #++
10
10
 
11
- require 'forwardable'
12
11
  require 'thread'
13
12
 
14
- module Awakenable
15
- def sleep (time = nil)
16
- @awakenable ||= IO.pipe
13
+ class ThreadPool
14
+ attr_reader :min, :max, :spawned
17
15
 
18
- begin
19
- @awakenable.first.read_nonblock 2048
20
- rescue Errno::EAGAIN; end
16
+ def initialize (min, max = nil, &block)
17
+ @min = min
18
+ @max = max || min
19
+ @block = block
21
20
 
22
- IO.select([@awakenable.first], nil, nil, time)
23
- end
21
+ @cond = ConditionVariable.new
22
+ @mutex = Mutex.new
24
23
 
25
- def wake_up
26
- @awakenable ||= IO.pipe
24
+ @todo = []
25
+ @workers = []
27
26
 
28
- @awakenable.last.write 'x'
29
- end
30
- end
27
+ @spawned = 0
28
+ @waiting = 0
29
+ @shutdown = false
30
+ @trim_requests = 0
31
+ @auto_trim = false
31
32
 
32
- class ThreadPool
33
- class Worker
34
- include Awakenable
35
-
36
- def initialize (pool)
37
- @pool = pool
38
- @mutex = Mutex.new
39
-
40
- @thread = Thread.new {
41
- loop do
42
- if @block
43
- begin
44
- @block.call(*@args)
45
- rescue Exception => e
46
- @pool.raise(e)
47
- end
33
+ @mutex.synchronize {
34
+ min.times {
35
+ spawn_thread
36
+ }
37
+ }
38
+ end
48
39
 
49
- @block = nil
50
- @args = nil
40
+ def auto_trim?; @auto_trim; end
41
+ def auto_trim!; @auto_trim = true; end
42
+ def no_auto_trim!; @auto_trim = false; end
51
43
 
52
- @pool.wake_up
53
- else
54
- sleep unless die?
55
- break if die?
56
- end
57
- end
58
- }
59
- end
44
+ def resize (min, max = nil)
45
+ @min = min
46
+ @max = max || min
60
47
 
61
- def available?
62
- @mutex.synchronize {
63
- !@block
64
- }
65
- end
48
+ trim!
49
+ end
66
50
 
67
- def process (*args, &block)
68
- return unless available?
51
+ def backlog
52
+ @mutex.synchronize {
53
+ @todo.length
54
+ }
55
+ end
69
56
 
70
- @mutex.synchronize {
71
- @block = block
72
- @args = args
57
+ def process (*args, &block)
58
+ @mutex.synchronize {
59
+ raise 'unable to add work while shutting down' if @shutdown
73
60
 
74
- wake_up
75
- }
76
- end
61
+ @todo << [args, block]
77
62
 
78
- def join
79
- @thread.join
80
- end
63
+ if @waiting == 0 && @spawned < @max
64
+ spawn_thread
65
+ end
81
66
 
82
- def kill
83
- return if die?
67
+ @cond.signal
68
+ }
69
+ end
84
70
 
85
- @die = true
86
- wake_up
87
- end
71
+ alias << process
88
72
 
89
- def die?
90
- @die
91
- end
73
+ def trim (force = false)
74
+ @mutex.synchronize {
75
+ if (force || @waiting > 0) && @spawned - @trim_requests > @min
76
+ @trim_requests -= 1
77
+ @cond.signal
78
+ end
79
+ }
80
+ end
92
81
 
93
- def dead?
94
- @thread.stop?
95
- end
82
+ def trim!
83
+ trim true
96
84
  end
97
85
 
98
- extend Forwardable
86
+ def shutdown
87
+ @mutex.synchronize {
88
+ @shutdown = true
89
+ @cond.broadcast
90
+ }
91
+
92
+ @workers.first.join until @workers.empty?
93
+ end
99
94
 
100
- attr_reader :watcher, :size
101
- def_delegators :@watcher, :sleep, :wake_up, :kill, :die?, :dead?, :join
102
- def_delegators :@current, :raise
95
+ private
96
+ def spawn_thread
97
+ @spawned += 1
103
98
 
104
- def initialize (size = 2)
105
- @size = 0
106
- @queue = Queue.new
107
- @pool = []
108
- @watcher = Worker.new(self)
109
- @current = Thread.current
110
-
111
- @watcher.process {
99
+ thread = Thread.new {
112
100
  loop do
113
- sleep if @queue.empty?
114
- next if @queue.empty?
101
+ work = nil
102
+ continue = true
115
103
 
116
- begin
117
- worker = @pool.find(&:available?) or sleep
118
- end until worker
104
+ @mutex.synchronize {
105
+ while @todo.empty?
106
+ if @trim_requests > 0
107
+ @trim_requests -= 1
108
+ continue = false
119
109
 
120
- break if die?
110
+ break
111
+ end
121
112
 
122
- args, block = @queue.pop
113
+ if @shutdown
114
+ continue = false
123
115
 
124
- worker.process(*args, &block)
125
- end
116
+ break
117
+ end
126
118
 
127
- @pool.each(&:kill)
128
- }
119
+ @waiting += 1
120
+ @cond.wait @mutex
121
+ @waiting -= 1
129
122
 
130
- resize(size)
131
- end
123
+ if @shutdown
124
+ continue = false
132
125
 
133
- def resize (size)
134
- return if @size == size
126
+ break
127
+ end
128
+ end
135
129
 
136
- if @size < size
137
- 1.upto(size - @size) {
138
- @pool << Worker.new(self)
139
- }
140
- else
141
- 1.upto(@size - size) {
142
- @pool.pop.kill
130
+ work = @todo.shift if continue
131
+ }
132
+
133
+ break unless continue
134
+
135
+ (work.last || @block).call(*work.first)
136
+
137
+ trim if auto_trim? && @spawned > @min
138
+ end
139
+
140
+ @mutex.synchronize {
141
+ @spawned -= 1
142
+ @workers.delete thread
143
143
  }
144
- end
144
+ }
145
145
 
146
- @size = size
147
- end
146
+ @workers << thread
148
147
 
149
- def process (*args, &block)
150
- @queue.push([args, block])
151
- @watcher.wake_up
152
- end; alias do process
148
+ thread
149
+ end
153
150
  end
data/threadpool.gemspec CHANGED
@@ -1,9 +1,9 @@
1
1
  Gem::Specification.new {|s|
2
2
  s.name = 'threadpool'
3
- s.version = '0.0.3'
3
+ s.version = '0.1.0'
4
4
  s.author = 'meh.'
5
5
  s.email = 'meh@paranoici.org'
6
- s.homepage = 'http://github.com/meh/threadpool'
6
+ s.homepage = 'http://github.com/meh/ruby-threadpool'
7
7
  s.platform = Gem::Platform::RUBY
8
8
  s.summary = 'A simple no-wasted-resources thread pool implementation.'
9
9
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: threadpool
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-12-05 00:00:00.000000000 Z
12
+ date: 2012-03-29 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description:
15
15
  email: meh@paranoici.org
@@ -20,7 +20,7 @@ files:
20
20
  - README.md
21
21
  - lib/threadpool.rb
22
22
  - threadpool.gemspec
23
- homepage: http://github.com/meh/threadpool
23
+ homepage: http://github.com/meh/ruby-threadpool
24
24
  licenses: []
25
25
  post_install_message:
26
26
  rdoc_options: []
@@ -40,7 +40,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
40
  version: '0'
41
41
  requirements: []
42
42
  rubyforge_project:
43
- rubygems_version: 1.8.10
43
+ rubygems_version: 1.8.11
44
44
  signing_key:
45
45
  specification_version: 3
46
46
  summary: A simple no-wasted-resources thread pool implementation.