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
@@ -1,5 +1,5 @@
1
1
  require 'thread'
2
- require 'timeout'
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
- @waiting = 0
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
- @waiting.times { @notifier << :set }
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 { @waiting += 1 }
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
- @notifier.pop
39
+ slept = sleep
41
40
  else
42
- Timeout::timeout(timeout) do
43
- @notifier.pop
44
- end
41
+ slept = sleep(timeout)
45
42
  end
46
- return true
47
- rescue Timeout::Error
48
- return false
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
@@ -3,7 +3,6 @@ require 'concurrent/global_thread_pool'
3
3
  module Concurrent
4
4
 
5
5
  class EventMachineDeferProxy
6
- behavior(:global_thread_pool)
7
6
 
8
7
  def post(*args, &block)
9
8
  if args.empty?
@@ -5,7 +5,6 @@ module Concurrent
5
5
 
6
6
  class Executor
7
7
  include Runnable
8
- behavior(:runnable)
9
8
 
10
9
  EXECUTION_INTERVAL = 60
11
10
  TIMEOUT_INTERVAL = 30
@@ -1,31 +1,128 @@
1
- require 'concurrent/thread_pool'
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 < AbstractThreadPool
6
- behavior(:global_thread_pool)
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
- super(opts.merge(max_threads: size))
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') unless block_given?
14
- return @mutex.synchronize do
15
- if @state == :running
16
- @queue << [args, block]
17
- @pool << create_worker_thread if @pool.size < @max_threads
18
- true
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
- false
59
+ @state = :shuttingdown
60
+ @pool.length.times{ @queue << :stop }
21
61
  end
22
62
  end
23
63
  end
24
64
 
25
- protected
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 collect_garbage
28
- @pool.reject! {|context| ! context.status }
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
@@ -11,8 +11,6 @@ module Concurrent
11
11
  include Observable
12
12
  include UsesGlobalThreadPool
13
13
 
14
- behavior(:future)
15
-
16
14
  def initialize(*args, &block)
17
15
  @mutex = Mutex.new
18
16
  unless block_given?
@@ -1,8 +1,24 @@
1
- require 'concurrent/cached_thread_pool'
1
+ module Concurrent
2
2
 
3
- $GLOBAL_THREAD_POOL ||= Concurrent::CachedThreadPool.new
3
+ class NullThreadPool
4
4
 
5
- module Concurrent
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
@@ -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
- 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
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
@@ -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 } unless block_given?
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? || ! block_given?
80
+ return self if fulfilled? || rescued? || block.nil?
84
81
  @lock.synchronize do
85
82
  rescuer = Rescuer.new(clazz, block)
86
83
  if pending?
@@ -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? || ! worker.behaves_as?(:runnable)
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