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.
- checksums.yaml +4 -4
- data/README.md +10 -33
- data/lib/concurrent.rb +5 -11
- data/lib/concurrent/{channel.rb → actor.rb} +14 -18
- data/lib/concurrent/agent.rb +5 -4
- data/lib/concurrent/cached_thread_pool.rb +116 -25
- data/lib/concurrent/cached_thread_pool/worker.rb +91 -0
- data/lib/concurrent/event.rb +13 -14
- data/lib/concurrent/event_machine_defer_proxy.rb +0 -1
- data/lib/concurrent/executor.rb +0 -1
- data/lib/concurrent/fixed_thread_pool.rb +111 -14
- data/lib/concurrent/fixed_thread_pool/worker.rb +54 -0
- data/lib/concurrent/future.rb +0 -2
- data/lib/concurrent/global_thread_pool.rb +21 -3
- data/lib/concurrent/goroutine.rb +1 -5
- data/lib/concurrent/obligation.rb +0 -19
- data/lib/concurrent/promise.rb +2 -5
- data/lib/concurrent/runnable.rb +2 -8
- data/lib/concurrent/supervisor.rb +9 -4
- data/lib/concurrent/utilities.rb +24 -0
- data/lib/concurrent/version.rb +1 -1
- data/md/agent.md +3 -3
- data/md/future.md +4 -4
- data/md/promise.md +15 -25
- data/md/thread_pool.md +9 -8
- data/spec/concurrent/actor_spec.rb +377 -0
- data/spec/concurrent/agent_spec.rb +2 -1
- data/spec/concurrent/cached_thread_pool_spec.rb +19 -29
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +1 -1
- data/spec/concurrent/event_spec.rb +1 -1
- data/spec/concurrent/executor_spec.rb +0 -8
- data/spec/concurrent/fixed_thread_pool_spec.rb +27 -16
- data/spec/concurrent/future_spec.rb +0 -13
- data/spec/concurrent/global_thread_pool_spec.rb +73 -0
- data/spec/concurrent/goroutine_spec.rb +0 -15
- data/spec/concurrent/obligation_shared.rb +1 -38
- data/spec/concurrent/promise_spec.rb +28 -47
- data/spec/concurrent/supervisor_spec.rb +1 -2
- data/spec/concurrent/thread_pool_shared.rb +28 -7
- data/spec/concurrent/utilities_spec.rb +50 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/functions.rb +17 -0
- metadata +12 -27
- data/lib/concurrent/functions.rb +0 -105
- data/lib/concurrent/null_thread_pool.rb +0 -25
- data/lib/concurrent/thread_pool.rb +0 -149
- data/md/reactor.md +0 -32
- data/spec/concurrent/channel_spec.rb +0 -446
- data/spec/concurrent/functions_spec.rb +0 -197
- data/spec/concurrent/null_thread_pool_spec.rb +0 -78
data/lib/concurrent/event.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'thread'
|
2
|
-
require '
|
2
|
+
require 'concurrent/utilities'
|
3
3
|
|
4
4
|
module Concurrent
|
5
5
|
|
@@ -7,9 +7,8 @@ module Concurrent
|
|
7
7
|
|
8
8
|
def initialize
|
9
9
|
@set = false
|
10
|
-
@notifier = Queue.new
|
11
10
|
@mutex = Mutex.new
|
12
|
-
@
|
11
|
+
@waiters = []
|
13
12
|
end
|
14
13
|
|
15
14
|
def set?
|
@@ -20,32 +19,32 @@ module Concurrent
|
|
20
19
|
return true if set?
|
21
20
|
@mutex.synchronize do
|
22
21
|
@set = true
|
23
|
-
@
|
24
|
-
@waiting = 0
|
22
|
+
@waiters.each {|waiter| waiter.run if waiter.status == 'sleep'}
|
25
23
|
end
|
26
24
|
return true
|
27
25
|
end
|
28
26
|
|
29
27
|
def reset
|
30
|
-
@mutex.synchronize { @set = false }
|
28
|
+
@mutex.synchronize { @set = false; @waiters.clear }
|
31
29
|
return true
|
32
30
|
end
|
33
31
|
|
34
32
|
def wait(timeout = nil)
|
35
33
|
return true if set?
|
36
34
|
|
37
|
-
@mutex.synchronize { @
|
35
|
+
@mutex.synchronize { @waiters << Thread.current }
|
36
|
+
return true if set? # if event was set while waiting for mutex
|
38
37
|
|
39
38
|
if timeout.nil?
|
40
|
-
|
39
|
+
slept = sleep
|
41
40
|
else
|
42
|
-
|
43
|
-
@notifier.pop
|
44
|
-
end
|
41
|
+
slept = sleep(timeout)
|
45
42
|
end
|
46
|
-
|
47
|
-
|
48
|
-
|
43
|
+
rescue
|
44
|
+
# let it fail
|
45
|
+
ensure
|
46
|
+
@mutex.synchronize { @waiters.delete(Thread.current) }
|
47
|
+
return set?
|
49
48
|
end
|
50
49
|
end
|
51
50
|
end
|
data/lib/concurrent/executor.rb
CHANGED
@@ -1,31 +1,128 @@
|
|
1
|
-
require '
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'concurrent/event'
|
4
|
+
require 'concurrent/fixed_thread_pool/worker'
|
2
5
|
|
3
6
|
module Concurrent
|
4
7
|
|
5
|
-
class FixedThreadPool
|
6
|
-
|
8
|
+
class FixedThreadPool
|
9
|
+
|
10
|
+
MIN_POOL_SIZE = 1
|
11
|
+
MAX_POOL_SIZE = 256
|
12
|
+
|
13
|
+
attr_accessor :max_threads
|
7
14
|
|
8
15
|
def initialize(size, opts = {})
|
9
|
-
|
16
|
+
@max_threads = size || MAX_POOL_SIZE
|
17
|
+
if @max_threads < MIN_POOL_SIZE || @max_threads > MAX_POOL_SIZE
|
18
|
+
raise ArgumentError.new("size must be from #{MIN_POOL_SIZE} to #{MAX_POOL_SIZE}")
|
19
|
+
end
|
20
|
+
|
21
|
+
@state = :running
|
22
|
+
@pool = []
|
23
|
+
@terminator = Event.new
|
24
|
+
@queue = Queue.new
|
25
|
+
@mutex = Mutex.new
|
26
|
+
end
|
27
|
+
|
28
|
+
def running?
|
29
|
+
return @state == :running
|
30
|
+
end
|
31
|
+
|
32
|
+
def wait_for_termination(timeout = nil)
|
33
|
+
return @terminator.wait(timeout)
|
10
34
|
end
|
11
35
|
|
12
36
|
def post(*args, &block)
|
13
|
-
raise ArgumentError.new('no block given')
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
37
|
+
raise ArgumentError.new('no block given') if block.nil?
|
38
|
+
@mutex.synchronize do
|
39
|
+
break false unless @state == :running
|
40
|
+
@queue << [args, block]
|
41
|
+
clean_pool
|
42
|
+
fill_pool
|
43
|
+
true
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def <<(block)
|
48
|
+
self.post(&block)
|
49
|
+
return self
|
50
|
+
end
|
51
|
+
|
52
|
+
def shutdown
|
53
|
+
@mutex.synchronize do
|
54
|
+
break unless @state == :running
|
55
|
+
if @pool.empty?
|
56
|
+
@state = :shutdown
|
57
|
+
@terminator.set
|
19
58
|
else
|
20
|
-
|
59
|
+
@state = :shuttingdown
|
60
|
+
@pool.length.times{ @queue << :stop }
|
21
61
|
end
|
22
62
|
end
|
23
63
|
end
|
24
64
|
|
25
|
-
|
65
|
+
def kill
|
66
|
+
@mutex.synchronize do
|
67
|
+
break if @state == :shutdown
|
68
|
+
@state = :shutdown
|
69
|
+
@queue.clear
|
70
|
+
drain_pool
|
71
|
+
@terminator.set
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def length
|
76
|
+
@mutex.synchronize do
|
77
|
+
@state == :running ? @pool.length : 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def create_worker_thread
|
82
|
+
wrkr = Worker.new(@queue, self)
|
83
|
+
Thread.new(wrkr, self) do |worker, parent|
|
84
|
+
Thread.current.abort_on_exception = false
|
85
|
+
worker.run
|
86
|
+
parent.on_worker_exit(worker)
|
87
|
+
end
|
88
|
+
return wrkr
|
89
|
+
end
|
90
|
+
|
91
|
+
def fill_pool
|
92
|
+
return unless @state == :running
|
93
|
+
while @pool.length < @max_threads
|
94
|
+
@pool << create_worker_thread
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def clean_pool
|
99
|
+
@pool.reject! {|worker| worker.dead? }
|
100
|
+
end
|
101
|
+
|
102
|
+
def drain_pool
|
103
|
+
@pool.each {|worker| worker.kill }
|
104
|
+
@pool.clear
|
105
|
+
end
|
26
106
|
|
27
|
-
def
|
28
|
-
|
107
|
+
def on_start_task(worker)
|
108
|
+
end
|
109
|
+
|
110
|
+
def on_end_task(worker)
|
111
|
+
@mutex.synchronize do
|
112
|
+
break unless @state == :running
|
113
|
+
clean_pool
|
114
|
+
fill_pool
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def on_worker_exit(worker)
|
119
|
+
@mutex.synchronize do
|
120
|
+
@pool.delete(worker)
|
121
|
+
if @pool.empty? && @state != :running
|
122
|
+
@state = :shutdown
|
123
|
+
@terminator.set
|
124
|
+
end
|
125
|
+
end
|
29
126
|
end
|
30
127
|
end
|
31
128
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
class FixedThreadPool
|
6
|
+
|
7
|
+
class Worker
|
8
|
+
|
9
|
+
def initialize(queue, parent)
|
10
|
+
@queue = queue
|
11
|
+
@parent = parent
|
12
|
+
@mutex = Mutex.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def dead?
|
16
|
+
return @mutex.synchronize do
|
17
|
+
@thread.nil? ? false : ! @thread.alive?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def kill
|
22
|
+
@mutex.synchronize do
|
23
|
+
Thread.kill(@thread) unless @thread.nil?
|
24
|
+
@thread = nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def run(thread = Thread.current)
|
29
|
+
@mutex.synchronize do
|
30
|
+
raise StandardError.new('already running') unless @thread.nil?
|
31
|
+
@thread = thread
|
32
|
+
end
|
33
|
+
|
34
|
+
loop do
|
35
|
+
task = @queue.pop
|
36
|
+
if task == :stop
|
37
|
+
@thread = nil
|
38
|
+
@parent.on_worker_exit(self)
|
39
|
+
break
|
40
|
+
end
|
41
|
+
|
42
|
+
@parent.on_start_task(self)
|
43
|
+
begin
|
44
|
+
task.last.call(*task.first)
|
45
|
+
rescue
|
46
|
+
# let it fail
|
47
|
+
ensure
|
48
|
+
@parent.on_end_task(self)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/concurrent/future.rb
CHANGED
@@ -1,8 +1,24 @@
|
|
1
|
-
|
1
|
+
module Concurrent
|
2
2
|
|
3
|
-
|
3
|
+
class NullThreadPool
|
4
4
|
|
5
|
-
|
5
|
+
def self.post(*args)
|
6
|
+
Thread.new(*args) do
|
7
|
+
Thread.current.abort_on_exception = false
|
8
|
+
yield(*args)
|
9
|
+
end
|
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
|
6
22
|
|
7
23
|
module UsesGlobalThreadPool
|
8
24
|
|
@@ -22,3 +38,5 @@ module Concurrent
|
|
22
38
|
end
|
23
39
|
end
|
24
40
|
end
|
41
|
+
|
42
|
+
$GLOBAL_THREAD_POOL ||= Concurrent::NullThreadPool.new
|
data/lib/concurrent/goroutine.rb
CHANGED
@@ -19,11 +19,7 @@ module Kernel
|
|
19
19
|
# @see https://gobyexample.com/goroutines
|
20
20
|
def go(*args, &block)
|
21
21
|
return false unless block_given?
|
22
|
-
|
23
|
-
args.first.post(*args.slice(1, args.length), &block)
|
24
|
-
else
|
25
|
-
$GLOBAL_THREAD_POOL.post(*args, &block)
|
26
|
-
end
|
22
|
+
$GLOBAL_THREAD_POOL.post(*args, &block)
|
27
23
|
end
|
28
24
|
module_function :go
|
29
25
|
end
|
@@ -1,27 +1,8 @@
|
|
1
1
|
require 'thread'
|
2
2
|
require 'timeout'
|
3
|
-
require 'functional'
|
4
3
|
|
5
4
|
require 'concurrent/event'
|
6
5
|
|
7
|
-
behavior_info(:future,
|
8
|
-
state: 0,
|
9
|
-
value: -1,
|
10
|
-
reason: 0,
|
11
|
-
pending?: 0,
|
12
|
-
fulfilled?: 0,
|
13
|
-
rejected?: 0)
|
14
|
-
|
15
|
-
behavior_info(:promise,
|
16
|
-
state: 0,
|
17
|
-
value: -1,
|
18
|
-
reason: 0,
|
19
|
-
pending?: 0,
|
20
|
-
fulfilled?: 0,
|
21
|
-
rejected?: 0,
|
22
|
-
then: 0,
|
23
|
-
rescue: -1)
|
24
|
-
|
25
6
|
module Concurrent
|
26
7
|
|
27
8
|
module Obligation
|
data/lib/concurrent/promise.rb
CHANGED
@@ -9,9 +9,6 @@ module Concurrent
|
|
9
9
|
include Obligation
|
10
10
|
include UsesGlobalThreadPool
|
11
11
|
|
12
|
-
behavior(:future)
|
13
|
-
behavior(:promise)
|
14
|
-
|
15
12
|
# Creates a new promise object. "A promise represents the eventual
|
16
13
|
# value returned from the single completion of an operation."
|
17
14
|
# Promises can be chained in a tree structure where each promise
|
@@ -60,7 +57,7 @@ module Concurrent
|
|
60
57
|
# @return [Promise] the new promise
|
61
58
|
def then(&block)
|
62
59
|
child = @lock.synchronize do
|
63
|
-
block = Proc.new{|result| result }
|
60
|
+
block = Proc.new{|result| result } if block.nil?
|
64
61
|
@children << Promise.new(self, &block)
|
65
62
|
@children.last.on_reject(@reason) if rejected?
|
66
63
|
push(@children.last)
|
@@ -80,7 +77,7 @@ module Concurrent
|
|
80
77
|
#
|
81
78
|
# @return [self] so that additional chaining can occur
|
82
79
|
def rescue(clazz = nil, &block)
|
83
|
-
return self if fulfilled? || rescued? ||
|
80
|
+
return self if fulfilled? || rescued? || block.nil?
|
84
81
|
@lock.synchronize do
|
85
82
|
rescuer = Rescuer.new(clazz, block)
|
86
83
|
if pending?
|
data/lib/concurrent/runnable.rb
CHANGED
@@ -1,10 +1,4 @@
|
|
1
1
|
require 'thread'
|
2
|
-
require 'functional'
|
3
|
-
|
4
|
-
behavior_info(:runnable,
|
5
|
-
run: 0,
|
6
|
-
stop: 0,
|
7
|
-
running?: 0)
|
8
2
|
|
9
3
|
module Concurrent
|
10
4
|
|
@@ -18,6 +12,8 @@ module Concurrent
|
|
18
12
|
Thread.abort_on_exception = false
|
19
13
|
runner.run
|
20
14
|
end
|
15
|
+
#HACK: more consistent on JRuby and Rbx
|
16
|
+
sleep(0.1)
|
21
17
|
end
|
22
18
|
end
|
23
19
|
|
@@ -52,8 +48,6 @@ module Concurrent
|
|
52
48
|
|
53
49
|
module Runnable
|
54
50
|
|
55
|
-
behavior(:runnable)
|
56
|
-
|
57
51
|
LifecycleError = Class.new(StandardError)
|
58
52
|
|
59
53
|
def self.included(base)
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require 'thread'
|
2
|
-
require 'functional'
|
3
2
|
|
4
3
|
require 'concurrent/runnable'
|
5
4
|
|
@@ -7,12 +6,11 @@ module Concurrent
|
|
7
6
|
|
8
7
|
class Supervisor
|
9
8
|
|
10
|
-
behavior(:runnable)
|
11
|
-
|
12
9
|
DEFAULT_MONITOR_INTERVAL = 1
|
13
10
|
RESTART_STRATEGIES = [:one_for_one, :one_for_all, :rest_for_one]
|
14
11
|
DEFAULT_MAX_RESTART = 5
|
15
12
|
DEFAULT_MAX_TIME = 60
|
13
|
+
WORKER_API = {run: 0, stop: 0, running?: 0}
|
16
14
|
|
17
15
|
CHILD_TYPES = [:worker, :supervisor]
|
18
16
|
CHILD_RESTART_OPTIONS = [:permanent, :transient, :temporary]
|
@@ -148,7 +146,7 @@ module Concurrent
|
|
148
146
|
end
|
149
147
|
|
150
148
|
def add_worker(worker, opts = {})
|
151
|
-
return nil if worker.nil? || !
|
149
|
+
return nil if worker.nil? || ! behaves_as_worker?(worker)
|
152
150
|
return @mutex.synchronize {
|
153
151
|
restart = opts[:restart] || :permanent
|
154
152
|
type = opts[:type] || (worker.is_a?(Supervisor) ? :supervisor : nil) || :worker
|
@@ -222,6 +220,13 @@ module Concurrent
|
|
222
220
|
|
223
221
|
private
|
224
222
|
|
223
|
+
def behaves_as_worker?(obj)
|
224
|
+
return WORKER_API.each do |method, arity|
|
225
|
+
break(false) unless obj.respond_to?(method) && obj.method(method).arity == arity
|
226
|
+
true
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
225
230
|
def monitor
|
226
231
|
@workers.each{|context| run_worker(context)}
|
227
232
|
loop do
|