concurrent-ruby 0.1.1 → 0.2.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -1
  3. data/lib/concurrent.rb +8 -1
  4. data/lib/concurrent/agent.rb +19 -40
  5. data/lib/concurrent/cached_thread_pool.rb +10 -11
  6. data/lib/concurrent/defer.rb +8 -12
  7. data/lib/concurrent/executor.rb +95 -0
  8. data/lib/concurrent/fixed_thread_pool.rb +12 -6
  9. data/lib/concurrent/functions.rb +120 -0
  10. data/lib/concurrent/future.rb +8 -20
  11. data/lib/concurrent/global_thread_pool.rb +13 -0
  12. data/lib/concurrent/goroutine.rb +5 -1
  13. data/lib/concurrent/null_thread_pool.rb +22 -0
  14. data/lib/concurrent/obligation.rb +10 -64
  15. data/lib/concurrent/promise.rb +38 -60
  16. data/lib/concurrent/reactor.rb +166 -0
  17. data/lib/concurrent/reactor/drb_async_demux.rb +83 -0
  18. data/lib/concurrent/reactor/tcp_sync_demux.rb +131 -0
  19. data/lib/concurrent/supervisor.rb +100 -0
  20. data/lib/concurrent/thread_pool.rb +16 -5
  21. data/lib/concurrent/utilities.rb +8 -0
  22. data/lib/concurrent/version.rb +1 -1
  23. data/md/defer.md +4 -4
  24. data/md/executor.md +187 -0
  25. data/md/promise.md +2 -0
  26. data/md/thread_pool.md +27 -0
  27. data/spec/concurrent/agent_spec.rb +8 -27
  28. data/spec/concurrent/cached_thread_pool_spec.rb +14 -1
  29. data/spec/concurrent/defer_spec.rb +17 -21
  30. data/spec/concurrent/event_machine_defer_proxy_spec.rb +159 -149
  31. data/spec/concurrent/executor_spec.rb +200 -0
  32. data/spec/concurrent/fixed_thread_pool_spec.rb +2 -3
  33. data/spec/concurrent/functions_spec.rb +217 -0
  34. data/spec/concurrent/future_spec.rb +4 -11
  35. data/spec/concurrent/global_thread_pool_spec.rb +38 -0
  36. data/spec/concurrent/goroutine_spec.rb +15 -0
  37. data/spec/concurrent/null_thread_pool_spec.rb +54 -0
  38. data/spec/concurrent/obligation_shared.rb +127 -116
  39. data/spec/concurrent/promise_spec.rb +16 -14
  40. data/spec/concurrent/reactor/drb_async_demux_spec.rb +196 -0
  41. data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +410 -0
  42. data/spec/concurrent/reactor_spec.rb +364 -0
  43. data/spec/concurrent/supervisor_spec.rb +258 -0
  44. data/spec/concurrent/thread_pool_shared.rb +156 -161
  45. data/spec/concurrent/utilities_spec.rb +30 -1
  46. data/spec/spec_helper.rb +13 -0
  47. metadata +38 -9
@@ -26,8 +26,10 @@ module Concurrent
26
26
  end
27
27
 
28
28
  def kill
29
- @status = :killed
30
- @pool.each{|t| Thread.kill(t) }
29
+ mutex.synchronize do
30
+ @status = :killed
31
+ @pool.each{|t| Thread.kill(t) }
32
+ end
31
33
  end
32
34
 
33
35
  def size
@@ -50,7 +52,9 @@ module Concurrent
50
52
 
51
53
  # @private
52
54
  def status # :nodoc:
53
- @pool.collect{|t| t.status }
55
+ mutex.synchronize do
56
+ @pool.collect{|t| t.status }
57
+ end
54
58
  end
55
59
 
56
60
  private
@@ -78,9 +82,11 @@ module Concurrent
78
82
  def collect_garbage # :nodoc:
79
83
  @collector = Thread.new do
80
84
  sleep(1)
81
- @pool.size.times do |i|
82
- if @pool[i].status.nil?
83
- @pool[i] = create_worker_thread
85
+ mutex.synchronize do
86
+ @pool.size.times do |i|
87
+ if @pool[i].status.nil?
88
+ @pool[i] = create_worker_thread
89
+ end
84
90
  end
85
91
  end
86
92
  end
@@ -0,0 +1,120 @@
1
+ require 'concurrent/agent'
2
+ require 'concurrent/defer'
3
+ require 'concurrent/future'
4
+ require 'concurrent/promise'
5
+
6
+ module Kernel
7
+
8
+ ## agent
9
+
10
+ def agent(initial, timeout = Concurrent::Agent::TIMEOUT)
11
+ return Concurrent::Agent.new(initial, timeout)
12
+ end
13
+ module_function :agent
14
+
15
+ def post(object, &block)
16
+ if object.respond_to?(:post)
17
+ return object.post(&block)
18
+ else
19
+ raise ArgumentError.new('object does not support #post')
20
+ end
21
+ end
22
+ module_function :post
23
+
24
+ ## defer
25
+
26
+ def defer(*args, &block)
27
+ return Concurrent::Defer.new(*args, &block)
28
+ end
29
+ module_function :defer
30
+
31
+ ## executor
32
+
33
+ def executor(*args, &block)
34
+ return Concurrent::Executor.run(*args, &block)
35
+ end
36
+ module_function :executor
37
+
38
+ ## future
39
+
40
+ def future(*args, &block)
41
+ return Concurrent::Future.new(*args, &block)
42
+ end
43
+ module_function :future
44
+
45
+ ## obligation
46
+
47
+ def deref(object, timeout = nil)
48
+ if object.respond_to?(:deref)
49
+ return object.deref(timeout)
50
+ elsif object.respond_to?(:value)
51
+ return object.value(timeout)
52
+ else
53
+ raise ArgumentError.new('object does not support #deref')
54
+ end
55
+ end
56
+ module_function :deref
57
+
58
+ def pending?(object)
59
+ if object.respond_to?(:pending?)
60
+ return object.pending?
61
+ else
62
+ raise ArgumentError.new('object does not support #pending?')
63
+ end
64
+ end
65
+ module_function :pending?
66
+
67
+ def fulfilled?(object)
68
+ if object.respond_to?(:fulfilled?)
69
+ return object.fulfilled?
70
+ elsif object.respond_to?(:realized?)
71
+ return object.realized?
72
+ else
73
+ raise ArgumentError.new('object does not support #fulfilled?')
74
+ end
75
+ end
76
+ module_function :fulfilled?
77
+
78
+ def realized?(object)
79
+ if object.respond_to?(:realized?)
80
+ return object.realized?
81
+ elsif object.respond_to?(:fulfilled?)
82
+ return object.fulfilled?
83
+ else
84
+ raise ArgumentError.new('object does not support #realized?')
85
+ end
86
+ end
87
+ module_function :realized?
88
+
89
+ def rejected?(object)
90
+ if object.respond_to?(:rejected?)
91
+ return object.rejected?
92
+ else
93
+ raise ArgumentError.new('object does not support #rejected?')
94
+ end
95
+ end
96
+ module_function :rejected?
97
+
98
+ ## promise
99
+
100
+ # Creates a new promise object. "A promise represents the eventual
101
+ # value returned from the single completion of an operation."
102
+ # Promises can be chained in a tree structure where each promise
103
+ # has zero or more children. Promises are resolved asynchronously
104
+ # in the order they are added to the tree. Parents are guaranteed
105
+ # to be resolved before their children. The result of each promise
106
+ # is passes to each of its children when the child resolves. When
107
+ # a promise is rejected all its children will be summarily rejected.
108
+ # A promise added to a rejected promise will immediately be rejected.
109
+ # A promise that is neither resolved or rejected is pending.
110
+ #
111
+ # @param args [Array] zero or more arguments for the block
112
+ # @param block [Proc] the block to call when attempting fulfillment
113
+ #
114
+ # @see Promise
115
+ # @see http://wiki.commonjs.org/wiki/Promises/A
116
+ def promise(*args, &block)
117
+ return Concurrent::Promise.new(*args, &block)
118
+ end
119
+ module_function :promise
120
+ end
@@ -8,17 +8,17 @@ module Concurrent
8
8
 
9
9
  class Future
10
10
  include Obligation
11
+ include UsesGlobalThreadPool
12
+
11
13
  behavior(:future)
12
14
 
13
15
  def initialize(*args, &block)
14
-
15
16
  unless block_given?
16
17
  @state = :fulfilled
17
18
  else
18
19
  @value = nil
19
20
  @state = :pending
20
- $GLOBAL_THREAD_POOL.post do
21
- Thread.pass
21
+ Future.thread_pool.post(*args) do
22
22
  work(*args, &block)
23
23
  end
24
24
  end
@@ -28,27 +28,15 @@ module Concurrent
28
28
 
29
29
  # @private
30
30
  def work(*args) # :nodoc:
31
- semaphore.synchronize do
31
+ mutex.synchronize do
32
32
  begin
33
- atomic {
34
- @value = yield(*args)
35
- @state = :fulfilled
36
- }
33
+ @value = yield(*args)
34
+ @state = :fulfilled
37
35
  rescue Exception => ex
38
- atomic {
39
- @state = :rejected
40
- @reason = ex
41
- }
36
+ @state = :rejected
37
+ @reason = ex
42
38
  end
43
39
  end
44
40
  end
45
41
  end
46
42
  end
47
-
48
- module Kernel
49
-
50
- def future(*args, &block)
51
- return Concurrent::Future.new(*args, &block)
52
- end
53
- module_function :future
54
- end
@@ -1,3 +1,16 @@
1
1
  require 'concurrent/cached_thread_pool'
2
2
 
3
3
  $GLOBAL_THREAD_POOL ||= Concurrent::CachedThreadPool.new
4
+
5
+ module Concurrent
6
+
7
+ module UsesGlobalThreadPool
8
+
9
+ def self.included(base)
10
+ class << base
11
+ attr_accessor :thread_pool
12
+ end
13
+ base.thread_pool = $GLOBAL_THREAD_POOL
14
+ end
15
+ end
16
+ end
@@ -19,7 +19,11 @@ module Kernel
19
19
  # @see https://gobyexample.com/goroutines
20
20
  def go(*args, &block)
21
21
  return false unless block_given?
22
- $GLOBAL_THREAD_POOL.post(*args, &block)
22
+ if args.first.behaves_as?(:global_thread_pool)
23
+ args.first.post(*args.slice(1, args.length), &block)
24
+ else
25
+ $GLOBAL_THREAD_POOL.post(*args, &block)
26
+ end
23
27
  end
24
28
  module_function :go
25
29
  end
@@ -0,0 +1,22 @@
1
+ require 'concurrent/global_thread_pool'
2
+
3
+ module Concurrent
4
+
5
+ class NullThreadPool
6
+ behavior(:global_thread_pool)
7
+
8
+ def self.post(*args, &block)
9
+ Thread.new(*args, &block)
10
+ return true
11
+ end
12
+
13
+ def post(*args, &block)
14
+ return NullThreadPool.post(*args, &block)
15
+ end
16
+
17
+ def <<(block)
18
+ NullThreadPool.post(&block)
19
+ return self
20
+ end
21
+ end
22
+ end
@@ -33,19 +33,23 @@ module Concurrent
33
33
  def fulfilled?() return(@state == :fulfilled); end
34
34
  alias_method :realized?, :fulfilled?
35
35
 
36
+ # Has the promise been rejected?
37
+ # @return [Boolean]
38
+ def rejected?() return(@state == :rejected); end
39
+
36
40
  # Is obligation completion still pending?
37
41
  # @return [Boolean]
38
- def pending?() return(!(fulfilled? || rejected?)); end
42
+ def pending?() return(@state == :pending); end
39
43
 
40
44
  def value(timeout = nil)
41
- if !pending? || timeout == 0
45
+ if timeout == 0 || ! pending?
42
46
  return @value
43
47
  elsif timeout.nil?
44
- return semaphore.synchronize { @value }
48
+ return mutex.synchronize { v = @value }
45
49
  else
46
50
  begin
47
51
  return Timeout::timeout(timeout.to_f) {
48
- semaphore.synchronize { @value }
52
+ mutex.synchronize { v = @value }
49
53
  }
50
54
  rescue Timeout::Error => ex
51
55
  return nil
@@ -54,68 +58,10 @@ module Concurrent
54
58
  end
55
59
  alias_method :deref, :value
56
60
 
57
- # Has the promise been rejected?
58
- # @return [Boolean]
59
- def rejected?() return(@state == :rejected); end
60
-
61
61
  protected
62
62
 
63
- def semaphore
64
- @semaphore ||= Mutex.new
65
- end
66
- end
67
- end
68
-
69
- module Kernel
70
-
71
- def deref(obligation, timeout = nil)
72
- if obligation.respond_to?(:deref)
73
- return obligation.deref(timeout)
74
- elsif obligation.respond_to?(:value)
75
- return obligation.deref(timeout)
76
- else
77
- return nil
78
- end
79
- end
80
- module_function :deref
81
-
82
- def pending?(obligation)
83
- if obligation.respond_to?(:pending?)
84
- return obligation.pending?
85
- else
86
- return false
87
- end
88
- end
89
- module_function :pending?
90
-
91
- def fulfilled?(obligation)
92
- if obligation.respond_to?(:fulfilled?)
93
- return obligation.fulfilled?
94
- elsif obligation.respond_to?(:realized?)
95
- return obligation.realized?
96
- else
97
- return false
98
- end
99
- end
100
- module_function :fulfilled?
101
-
102
- def realized?(obligation)
103
- if obligation.respond_to?(:realized?)
104
- return obligation.realized?
105
- elsif obligation.respond_to?(:fulfilled?)
106
- return obligation.fulfilled?
107
- else
108
- return false
109
- end
110
- end
111
- module_function :realized?
112
-
113
- def rejected?(obligation)
114
- if obligation.respond_to?(:rejected?)
115
- return obligation.rejected?
116
- else
117
- return false
63
+ def mutex
64
+ @mutex ||= Mutex.new
118
65
  end
119
66
  end
120
- module_function :rejected?
121
67
  end
@@ -1,5 +1,6 @@
1
1
  require 'thread'
2
2
 
3
+ require 'concurrent/global_thread_pool'
3
4
  require 'concurrent/obligation'
4
5
  require 'concurrent/utilities'
5
6
 
@@ -7,6 +8,8 @@ module Concurrent
7
8
 
8
9
  class Promise
9
10
  include Obligation
11
+ include UsesGlobalThreadPool
12
+
10
13
  behavior(:future)
11
14
  behavior(:promise)
12
15
 
@@ -33,17 +36,22 @@ module Concurrent
33
36
  @chain = [self]
34
37
  end
35
38
 
36
- @mutex = Mutex.new
39
+ @lock = Mutex.new
37
40
  @handler = block || Proc.new{|result| result }
38
41
  @state = :pending
39
42
  @value = nil
40
43
  @reason = nil
44
+ @rescued = false
41
45
  @children = []
42
46
  @rescuers = []
43
47
 
44
48
  realize(*args) if root?
45
49
  end
46
50
 
51
+ def rescued?
52
+ return @rescued
53
+ end
54
+
47
55
  # Create a new child Promise. The block argument for the child will
48
56
  # be the result of fulfilling its parent. If the child will
49
57
  # immediately be rejected if the parent has already been rejected.
@@ -52,7 +60,7 @@ module Concurrent
52
60
  #
53
61
  # @return [Promise] the new promise
54
62
  def then(&block)
55
- child = @mutex.synchronize do
63
+ child = @lock.synchronize do
56
64
  block = Proc.new{|result| result } unless block_given?
57
65
  @children << Promise.new(self, &block)
58
66
  @children.last.on_reject(@reason) if rejected?
@@ -73,8 +81,14 @@ module Concurrent
73
81
  #
74
82
  # @return [self] so that additional chaining can occur
75
83
  def rescue(clazz = Exception, &block)
76
- @mutex.synchronize do
77
- @rescuers << Rescuer.new(clazz, block) if block_given?
84
+ return self if fulfilled? || rescued? || ! block_given?
85
+ @lock.synchronize do
86
+ rescuer = Rescuer.new(clazz, block)
87
+ if pending?
88
+ @rescuers << rescuer
89
+ else
90
+ try_rescue(reason, rescuer)
91
+ end
78
92
  end
79
93
  return self
80
94
  end
@@ -90,15 +104,6 @@ module Concurrent
90
104
  # @private
91
105
  Rescuer = Struct.new(:clazz, :block)
92
106
 
93
- # @private
94
- def root # :nodoc:
95
- return atomic {
96
- current = self
97
- current = current.parent until current.root?
98
- current
99
- }
100
- end
101
-
102
107
  # @private
103
108
  def root? # :nodoc:
104
109
  @parent.nil?
@@ -115,47 +120,44 @@ module Concurrent
115
120
 
116
121
  # @private
117
122
  def on_fulfill(value) # :nodoc:
118
- @mutex.synchronize do
119
- if pending?
120
- @value = @handler.call(value)
121
- @state = :fulfilled
122
- @reason = nil
123
- end
123
+ @lock.synchronize do
124
+ @value = @handler.call(value)
125
+ @state = :fulfilled
126
+ @reason = nil
124
127
  end
125
128
  return @value
126
129
  end
127
130
 
128
131
  # @private
129
132
  def on_reject(reason) # :nodoc:
130
- @mutex.synchronize do
131
- if pending?
132
- @state = :rejected
133
- @reason = reason
134
- self.try_rescue(reason)
135
- @value = nil
136
- end
137
- @children.each{|child| child.on_reject(reason) }
138
- end
133
+ @value = nil
134
+ @state = :rejected
135
+ @reason = reason
136
+ try_rescue(reason)
137
+ @children.each{|child| child.on_reject(reason) }
139
138
  end
140
139
 
141
140
  # @private
142
- def try_rescue(ex) # :nodoc:
143
- rescuer = @rescuers.find{|r| ex.is_a?(r.clazz) }
144
- rescuer.block.call(ex) if rescuer
141
+ def try_rescue(ex, *rescuers) # :nodoc:
142
+ rescuers = @rescuers if rescuers.empty?
143
+ rescuer = rescuers.find{|r| ex.is_a?(r.clazz) }
144
+ if rescuer
145
+ rescuer.block.call(ex)
146
+ @rescued = true
147
+ end
145
148
  rescue Exception => e
146
149
  # supress
147
150
  end
148
151
 
149
152
  # @private
150
153
  def realize(*args) # :nodoc:
151
- Thread.new(@chain, @mutex, args) do |chain, mutex, args|
154
+ Promise.thread_pool.post(@chain, @lock, args) do |chain, lock, args|
152
155
  result = args.length == 1 ? args.first : args
153
156
  index = 0
154
157
  loop do
155
- Thread.pass
156
- current = mutex.synchronize{ chain[index] }
158
+ current = lock.synchronize{ chain[index] }
157
159
  unless current.rejected?
158
- current.semaphore.synchronize do
160
+ current.mutex.synchronize do
159
161
  begin
160
162
  result = current.on_fulfill(result)
161
163
  rescue Exception => ex
@@ -164,33 +166,9 @@ module Concurrent
164
166
  end
165
167
  end
166
168
  index += 1
167
- sleep while index >= chain.length
169
+ Thread.pass while index >= chain.length
168
170
  end
169
171
  end
170
172
  end
171
173
  end
172
174
  end
173
-
174
- module Kernel
175
-
176
- # Creates a new promise object. "A promise represents the eventual
177
- # value returned from the single completion of an operation."
178
- # Promises can be chained in a tree structure where each promise
179
- # has zero or more children. Promises are resolved asynchronously
180
- # in the order they are added to the tree. Parents are guaranteed
181
- # to be resolved before their children. The result of each promise
182
- # is passes to each of its children when the child resolves. When
183
- # a promise is rejected all its children will be summarily rejected.
184
- # A promise added to a rejected promise will immediately be rejected.
185
- # A promise that is neither resolved or rejected is pending.
186
- #
187
- # @param args [Array] zero or more arguments for the block
188
- # @param block [Proc] the block to call when attempting fulfillment
189
- #
190
- # @see Promise
191
- # @see http://wiki.commonjs.org/wiki/Promises/A
192
- def promise(*args, &block)
193
- return Concurrent::Promise.new(*args, &block)
194
- end
195
- module_function :promise
196
- end