concurrent-ruby 0.3.0.pre.1 → 0.3.0.pre.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -33
  3. data/lib/concurrent.rb +5 -11
  4. data/lib/concurrent/{channel.rb → actor.rb} +14 -18
  5. data/lib/concurrent/agent.rb +5 -4
  6. data/lib/concurrent/cached_thread_pool.rb +116 -25
  7. data/lib/concurrent/cached_thread_pool/worker.rb +91 -0
  8. data/lib/concurrent/event.rb +13 -14
  9. data/lib/concurrent/event_machine_defer_proxy.rb +0 -1
  10. data/lib/concurrent/executor.rb +0 -1
  11. data/lib/concurrent/fixed_thread_pool.rb +111 -14
  12. data/lib/concurrent/fixed_thread_pool/worker.rb +54 -0
  13. data/lib/concurrent/future.rb +0 -2
  14. data/lib/concurrent/global_thread_pool.rb +21 -3
  15. data/lib/concurrent/goroutine.rb +1 -5
  16. data/lib/concurrent/obligation.rb +0 -19
  17. data/lib/concurrent/promise.rb +2 -5
  18. data/lib/concurrent/runnable.rb +2 -8
  19. data/lib/concurrent/supervisor.rb +9 -4
  20. data/lib/concurrent/utilities.rb +24 -0
  21. data/lib/concurrent/version.rb +1 -1
  22. data/md/agent.md +3 -3
  23. data/md/future.md +4 -4
  24. data/md/promise.md +15 -25
  25. data/md/thread_pool.md +9 -8
  26. data/spec/concurrent/actor_spec.rb +377 -0
  27. data/spec/concurrent/agent_spec.rb +2 -1
  28. data/spec/concurrent/cached_thread_pool_spec.rb +19 -29
  29. data/spec/concurrent/event_machine_defer_proxy_spec.rb +1 -1
  30. data/spec/concurrent/event_spec.rb +1 -1
  31. data/spec/concurrent/executor_spec.rb +0 -8
  32. data/spec/concurrent/fixed_thread_pool_spec.rb +27 -16
  33. data/spec/concurrent/future_spec.rb +0 -13
  34. data/spec/concurrent/global_thread_pool_spec.rb +73 -0
  35. data/spec/concurrent/goroutine_spec.rb +0 -15
  36. data/spec/concurrent/obligation_shared.rb +1 -38
  37. data/spec/concurrent/promise_spec.rb +28 -47
  38. data/spec/concurrent/supervisor_spec.rb +1 -2
  39. data/spec/concurrent/thread_pool_shared.rb +28 -7
  40. data/spec/concurrent/utilities_spec.rb +50 -0
  41. data/spec/spec_helper.rb +0 -1
  42. data/spec/support/functions.rb +17 -0
  43. metadata +12 -27
  44. data/lib/concurrent/functions.rb +0 -105
  45. data/lib/concurrent/null_thread_pool.rb +0 -25
  46. data/lib/concurrent/thread_pool.rb +0 -149
  47. data/md/reactor.md +0 -32
  48. data/spec/concurrent/channel_spec.rb +0 -446
  49. data/spec/concurrent/functions_spec.rb +0 -197
  50. data/spec/concurrent/null_thread_pool_spec.rb +0 -78
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed299ebc57fa2caa62d5538e28f6506d175be4f0
4
- data.tar.gz: 4e160d0efeefab15be0c1767679079f9d07b5b59
3
+ metadata.gz: 2e3a37abe10cd8c068be7f5f34ec87ea665c4d8c
4
+ data.tar.gz: bbb6cf6902175de9995656c9ded8e611940edfb7
5
5
  SHA512:
6
- metadata.gz: 5687e0000024468f66f42cfd4742ff672502ed6f9cde7a9579a84acfdcdfde8af33b2df28a0b387f36767bfefe3d70411b8efa7a5ba8cb3f7a684112db5a11ec
7
- data.tar.gz: d8e45ab66ecb79ad64310e8a31f604c146840d9abf5bd51e9dcbf48279767dd0c9a02fad2f37e5bef25f9ec5ca656d92bf87ccbcced9e2aa7ff9bc8e9e47fca5
6
+ metadata.gz: 69f7670740cfb70d858462864993d38209050a435435cbd4a9202dda5146f3ab32629bc6e2206b016c2ec0d9a95b5ff1c7751a2ee8278ba6e6400fafb478d23f
7
+ data.tar.gz: 84ce8fdad2b1ccc72f93e6a5b08889937b9c27d2d4aea0560d191ac4e9338e5909fedbd1a9a128c228a36eb7f6fbcc6f3206130308658122d23d98e89a2a4239
data/README.md CHANGED
@@ -55,12 +55,13 @@ Several features from Erlang, Go, Clojure, Java, and JavaScript have been implem
55
55
 
56
56
  * Clojure inspired [Agent](https://github.com/jdantonio/concurrent-ruby/blob/master/md/agent.md)
57
57
  * Clojure inspired [Future](https://github.com/jdantonio/concurrent-ruby/blob/master/md/future.md)
58
+ * Scala inspired [Actor](http://www.scala-lang.org/api/current/index.html#scala.actors.Actor)
58
59
  * Go inspired [Goroutine](https://github.com/jdantonio/concurrent-ruby/blob/master/md/goroutine.md)
59
60
  * JavaScript inspired [Promise](https://github.com/jdantonio/concurrent-ruby/blob/master/md/promise.md)
60
61
  * Java inspired [Thread Pools](https://github.com/jdantonio/concurrent-ruby/blob/master/md/thread_pool.md)
61
62
  * Old school [events](http://msdn.microsoft.com/en-us/library/windows/desktop/ms682655(v=vs.85).aspx) from back in my Visual C++ days
62
63
  * Scheduled task execution with the [Executor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/executor.md) service
63
- * Erlang-inspired [Supervisor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/supervisor.md) for managing long-running threads
64
+ * Erlang inspired [Supervisor](https://github.com/jdantonio/concurrent-ruby/blob/master/md/supervisor.md) for managing long-running threads
64
65
 
65
66
  ### Is it any good?
66
67
 
@@ -68,11 +69,10 @@ Several features from Erlang, Go, Clojure, Java, and JavaScript have been implem
68
69
 
69
70
  ### Supported Ruby versions
70
71
 
71
- MRI 1.9.2, 1.9.3, and 2.0. This library is pure Ruby and has minimal gem dependencies. It should be
72
- fully compatible with any Ruby interpreter that is 1.9.x compliant. I simply don't know enough
73
- about JRuby, Rubinius, or the others to fully support them. I can promise good karma and
74
- attribution on this page to anyone wishing to take responsibility for verifying compaitibility
75
- with any Ruby other than MRI.
72
+ MRI 1.9.2, 1.9.3, 2.0, 2.1, and JRuby (1.9 mode). This library is pure Ruby and has no gem dependencies.
73
+ It should be fully compatible with any Ruby interpreter that is 1.9.x compliant. I simply don't know enough
74
+ about Rubinius or the others to fully support them. I can promise good karma and attribution on this page
75
+ to anyone wishing to take responsibility for verifying compaitibility with any Ruby other than MRI.
76
76
 
77
77
  ### Install
78
78
 
@@ -94,25 +94,6 @@ Once you've installed the gem you must `require` it in your project:
94
94
  require 'concurrent'
95
95
  ```
96
96
 
97
- ### Kernel Methods
98
-
99
- Many Ruby developers consider it bad form to add function to the global (Kernel) namespace.
100
- I don't necessarily agree. If the function acts like a low-level feature of the language
101
- I think it is OK to add the method to the `Kernel` module. To support my personal programming
102
- style I have chosen to implement `Kernel` methods to instance many of the objects in this
103
- library. Out of respect for the larger Ruby community I have made these methods optional.
104
- They are not imported with the normal `require 'concurrent'` directive. To import these
105
- functions you must import the `concurrent/functions` library.
106
-
107
- ```ruby
108
- require 'concurrent'
109
- score = agent(10) #=> NoMethodError: undefined method `agent' for main:Object
110
-
111
- require 'concurrent/functions'
112
- score = agent(10) #=> #<Concurrent::Agent:0x35b2b28 ...
113
- score.value #=> 10
114
- ```
115
-
116
97
  ### Examples
117
98
 
118
99
  For complete examples, see the specific documentation linked above. Below are a few examples to whet your appetite.
@@ -139,9 +120,8 @@ sleep(0.5)
139
120
 
140
121
  ```ruby
141
122
  require 'concurrent'
142
- require 'concurrent/functions'
143
123
 
144
- score = agent(10)
124
+ score = Concurrent::Agent.new(10)
145
125
  score.value #=> 10
146
126
 
147
127
  score << proc{|current| current + 100 }
@@ -150,7 +130,7 @@ score.value #=> 110
150
130
 
151
131
  score << proc{|current| current * 2 }
152
132
  sleep(0.1)
153
- deref score #=> 220
133
+ score.value #=> 220
154
134
 
155
135
  score << proc{|current| current - 50 }
156
136
  sleep(0.1)
@@ -161,22 +141,19 @@ score.value #=> 170
161
141
 
162
142
  ```ruby
163
143
  require 'concurrent'
164
- require 'concurrent/functions'
165
144
 
166
- count = future{ sleep(1); 10 }
145
+ count = Concurrent::Future.new{ sleep(1); 10 }
167
146
  count.state #=> :pending
168
147
  # do stuff...
169
148
  count.value #=> 10 (after blocking)
170
- deref count #=> 10
171
149
  ```
172
150
 
173
151
  #### Promise (JavaScript)
174
152
 
175
153
  ```ruby
176
154
  require 'concurrent'
177
- require 'concurrent/functions'
178
155
 
179
- p = promise("Jerry", "D'Antonio"){|a, b| "#{a} #{b}" }.
156
+ p = Concurrent::Promise.new("Jerry", "D'Antonio"){|a, b| "#{a} #{b}" }.
180
157
  then{|result| "Hello #{result}." }.
181
158
  rescue(StandardError){|ex| puts "Boom!" }.
182
159
  then{|result| "#{result} Would you like to play a game?"}
@@ -1,14 +1,9 @@
1
- require 'thread'
2
-
3
- $ENABLE_BEHAVIOR_CHECK_ON_CONSTRUCTION ||= ( $DEGUG ? true : false )
4
- require 'functional'
5
-
6
1
  require 'concurrent/version'
7
2
 
8
- require 'concurrent/event'
9
3
 
4
+ require 'concurrent/actor'
10
5
  require 'concurrent/agent'
11
- require 'concurrent/channel'
6
+ require 'concurrent/event'
12
7
  require 'concurrent/executor'
13
8
  require 'concurrent/future'
14
9
  require 'concurrent/goroutine'
@@ -16,12 +11,11 @@ require 'concurrent/obligation'
16
11
  require 'concurrent/promise'
17
12
  require 'concurrent/runnable'
18
13
  require 'concurrent/supervisor'
14
+ require 'concurrent/utilities'
15
+
16
+ require 'concurrent/global_thread_pool'
19
17
 
20
- require 'concurrent/thread_pool'
21
18
  require 'concurrent/cached_thread_pool'
22
19
  require 'concurrent/fixed_thread_pool'
23
- require 'concurrent/null_thread_pool'
24
-
25
- require 'concurrent/global_thread_pool'
26
20
 
27
21
  require 'concurrent/event_machine_defer_proxy' if defined?(EventMachine)
@@ -5,15 +5,13 @@ require 'concurrent/runnable'
5
5
 
6
6
  module Concurrent
7
7
 
8
- class Channel
8
+ # http://www.scala-lang.org/api/current/index.html#scala.actors.Actor
9
+ class Actor
9
10
  include Observable
10
11
  include Runnable
11
- behavior(:runnable)
12
12
 
13
- def initialize(errorback = nil, &block)
13
+ def initialize
14
14
  @queue = Queue.new
15
- @task = block
16
- @errorback = errorback
17
15
  end
18
16
 
19
17
  def post(*message)
@@ -27,19 +25,23 @@ module Concurrent
27
25
  return self
28
26
  end
29
27
 
30
- def self.pool(count, errorback = nil, &block)
28
+ def self.pool(count, &block)
31
29
  raise ArgumentError.new('count must be greater than zero') unless count > 0
32
30
  mailbox = Queue.new
33
- channels = count.times.collect do
34
- channel = self.new(errorback, &block)
35
- channel.instance_variable_set(:@queue, mailbox)
36
- channel
31
+ actors = count.times.collect do
32
+ actor = self.new(&block)
33
+ actor.instance_variable_set(:@queue, mailbox)
34
+ actor
37
35
  end
38
- return Poolbox.new(mailbox), channels
36
+ return Poolbox.new(mailbox), actors
39
37
  end
40
38
 
41
39
  protected
42
40
 
41
+ def act(*args)
42
+ raise NotImplementedError.new("#{self.class} does not implement #act")
43
+ end
44
+
43
45
  class Poolbox
44
46
 
45
47
  def initialize(queue)
@@ -73,7 +75,7 @@ module Concurrent
73
75
  message = @queue.pop
74
76
  return if message == :stop
75
77
  begin
76
- result = receive(*message)
78
+ result = act(*message)
77
79
  changed
78
80
  notify_observers(Time.now, message, result)
79
81
  rescue => ex
@@ -83,12 +85,6 @@ module Concurrent
83
85
 
84
86
  # @private
85
87
  def on_error(time, msg, ex) # :nodoc:
86
- @errorback.call(time, msg, ex) if @errorback
87
- end
88
-
89
- # @private
90
- def receive(*message) # :nodoc:
91
- @task.call(*message) unless @task.nil?
92
88
  end
93
89
  end
94
90
  end
@@ -2,6 +2,7 @@ require 'thread'
2
2
  require 'observer'
3
3
 
4
4
  require 'concurrent/global_thread_pool'
5
+ require 'concurrent/utilities'
5
6
 
6
7
  module Concurrent
7
8
 
@@ -45,7 +46,7 @@ module Concurrent
45
46
  alias_method :deref, :value
46
47
 
47
48
  def rescue(clazz = nil, &block)
48
- if block_given?
49
+ unless block.nil?
49
50
  @mutex.synchronize do
50
51
  @rescuers << Rescuer.new(clazz, block)
51
52
  end
@@ -56,7 +57,7 @@ module Concurrent
56
57
  alias_method :on_error, :rescue
57
58
 
58
59
  def validate(&block)
59
- @validator = block if block_given?
60
+ @validator = block unless block.nil?
60
61
  return self
61
62
  end
62
63
  alias_method :validates, :validate
@@ -64,7 +65,7 @@ module Concurrent
64
65
  alias_method :validates_with, :validate
65
66
 
66
67
  def post(&block)
67
- Agent.thread_pool.post{ work(&block) } if block_given?
68
+ Agent.thread_pool.post{ work(&block) } unless block.nil?
68
69
  end
69
70
 
70
71
  def <<(block)
@@ -93,7 +94,7 @@ module Concurrent
93
94
  def work(&handler) # :nodoc:
94
95
  begin
95
96
  @mutex.synchronize do
96
- result = Timeout.timeout(@timeout) do
97
+ result = Concurrent::timeout(@timeout) do
97
98
  handler.call(@value)
98
99
  end
99
100
  if @validator.nil? || @validator.call(result)
@@ -1,51 +1,142 @@
1
- require 'concurrent/thread_pool'
1
+ require 'thread'
2
+
3
+ require 'concurrent/event'
4
+ require 'concurrent/cached_thread_pool/worker'
2
5
 
3
6
  module Concurrent
4
7
 
5
- class CachedThreadPool < AbstractThreadPool
6
- behavior(:global_thread_pool)
8
+ class CachedThreadPool
9
+
10
+ MIN_POOL_SIZE = 1
11
+ MAX_POOL_SIZE = 256
7
12
 
8
13
  DEFAULT_THREAD_IDLETIME = 60
9
14
 
15
+ attr_accessor :max_threads
16
+
10
17
  def initialize(opts = {})
11
18
  @idletime = (opts[:idletime] || DEFAULT_THREAD_IDLETIME).to_i
12
- super(opts)
19
+ raise ArgumentError.new('idletime must be greater than zero') if @idletime <= 0
20
+
21
+ @max_threads = opts[:max_threads] || opts[:max] || MAX_POOL_SIZE
22
+ if @max_threads < MIN_POOL_SIZE || @max_threads > MAX_POOL_SIZE
23
+ raise ArgumentError.new("size must be from #{MIN_POOL_SIZE} to #{MAX_POOL_SIZE}")
24
+ end
25
+
26
+ @state = :running
27
+ @pool = []
28
+ @terminator = Event.new
29
+ @mutex = Mutex.new
30
+
31
+ @busy = []
32
+ @idle = []
33
+ end
34
+
35
+ def <<(block)
36
+ self.post(&block)
37
+ return self
13
38
  end
14
39
 
15
40
  def post(*args, &block)
16
- raise ArgumentError.new('no block given') unless block_given?
17
- return @mutex.synchronize do
18
- if @state == :running
19
- @queue << [args, block]
20
- at_capacity = @pool.empty? || ! @queue.empty? || @working >= @pool.size
21
- if at_capacity && @pool.length < @max_threads
22
- @pool << create_worker_thread
41
+ raise ArgumentError.new('no block given') if block.nil?
42
+ @mutex.synchronize do
43
+ break false unless @state == :running
44
+
45
+ if @idle.empty?
46
+ if @idle.length + @busy.length < @max_threads
47
+ worker = create_worker_thread
48
+ else
49
+ worker = @busy.shift
23
50
  end
24
- true
25
51
  else
26
- false
52
+ worker = @idle.pop
27
53
  end
54
+
55
+ @busy.push(worker)
56
+ worker.signal(*args, &block)
57
+
58
+ prune_stale_workers
59
+ true
28
60
  end
29
61
  end
30
62
 
31
- protected
63
+ def running?
64
+ return @state == :running
65
+ end
66
+
67
+ def wait_for_termination(timeout = nil)
68
+ return @terminator.wait(timeout)
69
+ end
70
+
71
+ def shutdown
72
+ @mutex.synchronize do
73
+ break unless @state == :running
74
+ if @idle.empty? && @busy.empty?
75
+ @state = :shutdown
76
+ @terminator.set
77
+ else
78
+ @state = :shuttingdown
79
+ @idle.each{|worker| worker.stop }
80
+ @busy.each{|worker| worker.stop }
81
+ end
82
+ end
83
+ end
84
+
85
+ def kill
86
+ @mutex.synchronize do
87
+ break if @state == :shutdown
88
+ @state = :shutdown
89
+ @idle.each{|worker| worker.kill }
90
+ @busy.each{|worker| worker.kill }
91
+ @terminator.set
92
+ end
93
+ end
32
94
 
33
- def dead_worker?(context)
34
- return context.thread.nil? || context.thread.status == 'aborting' || ! context.thread.status
95
+ def length
96
+ @mutex.synchronize do
97
+ @state == :running ? @busy.length + @idle.length : 0
98
+ end
35
99
  end
36
100
 
37
- def stale_worker?(context)
38
- if context.status == :idle && @idletime <= (timestamp - context.idletime)
39
- context.thread.kill
40
- return true
41
- else
42
- return false
101
+ def on_worker_exit(worker)
102
+ @mutex.synchronize do
103
+ @idle.delete(worker)
104
+ @busy.delete(worker)
105
+ if @idle.empty? && @busy.empty? && @state != :running
106
+ @state = :shutdown
107
+ @terminator.set
108
+ end
43
109
  end
44
110
  end
45
111
 
46
- def collect_garbage
47
- @pool.reject! do |context|
48
- dead_worker?(context) || stale_worker?(context)
112
+ def on_end_task(worker)
113
+ @mutex.synchronize do
114
+ break unless @state == :running
115
+ @busy.delete(worker)
116
+ @idle.push(worker)
117
+ end
118
+ end
119
+
120
+ protected
121
+
122
+ def create_worker_thread
123
+ wrkr = Worker.new(self)
124
+ Thread.new(wrkr, self) do |worker, parent|
125
+ Thread.current.abort_on_exception = false
126
+ worker.run
127
+ parent.on_worker_exit(worker)
128
+ end
129
+ return wrkr
130
+ end
131
+
132
+ def prune_stale_workers
133
+ @idle.reject! do |worker|
134
+ if worker.idletime > @idletime
135
+ worker.stop
136
+ true
137
+ else
138
+ worker.dead?
139
+ end
49
140
  end
50
141
  end
51
142
  end
@@ -0,0 +1,91 @@
1
+ require 'thread'
2
+
3
+ module Concurrent
4
+
5
+ class CachedThreadPool
6
+
7
+ class Worker
8
+
9
+ def initialize(parent)
10
+ @parent = parent
11
+ @mutex = Mutex.new
12
+ @idletime = Time.now
13
+ @resource = ConditionVariable.new
14
+ @tasks = Queue.new
15
+ end
16
+
17
+ def idle?
18
+ return ! @idletime.nil?
19
+ end
20
+
21
+ def dead?
22
+ return @mutex.synchronize do
23
+ @thread.nil? ? false : ! @thread.alive?
24
+ end
25
+ end
26
+
27
+ def idletime
28
+ return @mutex.synchronize do
29
+ @idletime.nil? ? 0 : Time.now.to_i - @idletime.to_i
30
+ end
31
+ end
32
+
33
+ def signal(*args, &block)
34
+ return @mutex.synchronize do
35
+ break(false) if @parent.nil?
36
+ @tasks << [args, block]
37
+ @resource.signal
38
+ true
39
+ end
40
+ end
41
+
42
+ def stop
43
+ return @mutex.synchronize do
44
+ @tasks.clear
45
+ @tasks << :stop
46
+ @resource.signal
47
+ end
48
+ end
49
+
50
+ def kill
51
+ @mutex.synchronize do
52
+ @idletime = Time.now
53
+ @parent = nil
54
+ Thread.kill(@thread) unless @thread.nil?
55
+ @thread = nil
56
+ end
57
+ end
58
+
59
+ def run(thread = Thread.current)
60
+ @mutex.synchronize do
61
+ raise StandardError.new('already running') unless @thread.nil?
62
+ @thread = thread
63
+ end
64
+
65
+ loop do
66
+ task = @mutex.synchronize do
67
+ @resource.wait(@mutex, 60) if @tasks.empty?
68
+
69
+ @tasks.pop(true)
70
+ end
71
+
72
+ if task == :stop
73
+ @thread = nil
74
+ @parent.on_worker_exit(self)
75
+ @parent = nil
76
+ break
77
+ end
78
+
79
+ #@parent.on_start_task(self)
80
+ begin
81
+ task.last.call(*task.first)
82
+ rescue
83
+ # let it fail
84
+ ensure
85
+ @parent.on_end_task(self)
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end