threadpool 0.0.3 → 0.1.0

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.
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.