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.
- checksums.yaml +4 -4
- data/README.md +48 -1
- data/lib/concurrent.rb +8 -1
- data/lib/concurrent/agent.rb +19 -40
- data/lib/concurrent/cached_thread_pool.rb +10 -11
- data/lib/concurrent/defer.rb +8 -12
- data/lib/concurrent/executor.rb +95 -0
- data/lib/concurrent/fixed_thread_pool.rb +12 -6
- data/lib/concurrent/functions.rb +120 -0
- data/lib/concurrent/future.rb +8 -20
- data/lib/concurrent/global_thread_pool.rb +13 -0
- data/lib/concurrent/goroutine.rb +5 -1
- data/lib/concurrent/null_thread_pool.rb +22 -0
- data/lib/concurrent/obligation.rb +10 -64
- data/lib/concurrent/promise.rb +38 -60
- data/lib/concurrent/reactor.rb +166 -0
- data/lib/concurrent/reactor/drb_async_demux.rb +83 -0
- data/lib/concurrent/reactor/tcp_sync_demux.rb +131 -0
- data/lib/concurrent/supervisor.rb +100 -0
- data/lib/concurrent/thread_pool.rb +16 -5
- data/lib/concurrent/utilities.rb +8 -0
- data/lib/concurrent/version.rb +1 -1
- data/md/defer.md +4 -4
- data/md/executor.md +187 -0
- data/md/promise.md +2 -0
- data/md/thread_pool.md +27 -0
- data/spec/concurrent/agent_spec.rb +8 -27
- data/spec/concurrent/cached_thread_pool_spec.rb +14 -1
- data/spec/concurrent/defer_spec.rb +17 -21
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +159 -149
- data/spec/concurrent/executor_spec.rb +200 -0
- data/spec/concurrent/fixed_thread_pool_spec.rb +2 -3
- data/spec/concurrent/functions_spec.rb +217 -0
- data/spec/concurrent/future_spec.rb +4 -11
- data/spec/concurrent/global_thread_pool_spec.rb +38 -0
- data/spec/concurrent/goroutine_spec.rb +15 -0
- data/spec/concurrent/null_thread_pool_spec.rb +54 -0
- data/spec/concurrent/obligation_shared.rb +127 -116
- data/spec/concurrent/promise_spec.rb +16 -14
- data/spec/concurrent/reactor/drb_async_demux_spec.rb +196 -0
- data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +410 -0
- data/spec/concurrent/reactor_spec.rb +364 -0
- data/spec/concurrent/supervisor_spec.rb +258 -0
- data/spec/concurrent/thread_pool_shared.rb +156 -161
- data/spec/concurrent/utilities_spec.rb +30 -1
- data/spec/spec_helper.rb +13 -0
- metadata +38 -9
@@ -26,8 +26,10 @@ module Concurrent
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def kill
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
82
|
-
|
83
|
-
@pool[i]
|
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
|
data/lib/concurrent/future.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
31
|
+
mutex.synchronize do
|
32
32
|
begin
|
33
|
-
|
34
|
-
|
35
|
-
@state = :fulfilled
|
36
|
-
}
|
33
|
+
@value = yield(*args)
|
34
|
+
@state = :fulfilled
|
37
35
|
rescue Exception => ex
|
38
|
-
|
39
|
-
|
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
|
data/lib/concurrent/goroutine.rb
CHANGED
@@ -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
|
-
|
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(
|
42
|
+
def pending?() return(@state == :pending); end
|
39
43
|
|
40
44
|
def value(timeout = nil)
|
41
|
-
if
|
45
|
+
if timeout == 0 || ! pending?
|
42
46
|
return @value
|
43
47
|
elsif timeout.nil?
|
44
|
-
return
|
48
|
+
return mutex.synchronize { v = @value }
|
45
49
|
else
|
46
50
|
begin
|
47
51
|
return Timeout::timeout(timeout.to_f) {
|
48
|
-
|
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
|
64
|
-
@
|
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
|
data/lib/concurrent/promise.rb
CHANGED
@@ -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
|
-
@
|
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 = @
|
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
|
-
|
77
|
-
|
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
|
-
@
|
119
|
-
|
120
|
-
|
121
|
-
|
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
|
-
@
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
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
|
-
|
144
|
-
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
|
-
|
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
|
-
|
156
|
-
current = mutex.synchronize{ chain[index] }
|
158
|
+
current = lock.synchronize{ chain[index] }
|
157
159
|
unless current.rejected?
|
158
|
-
current.
|
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
|
-
|
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
|