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