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

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