concurrent-ruby 0.4.1 → 0.5.0.pre.1

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -33
  3. data/lib/concurrent.rb +11 -3
  4. data/lib/concurrent/actor.rb +29 -29
  5. data/lib/concurrent/agent.rb +98 -16
  6. data/lib/concurrent/atomic.rb +125 -0
  7. data/lib/concurrent/channel.rb +36 -1
  8. data/lib/concurrent/condition.rb +67 -0
  9. data/lib/concurrent/copy_on_notify_observer_set.rb +80 -0
  10. data/lib/concurrent/copy_on_write_observer_set.rb +94 -0
  11. data/lib/concurrent/count_down_latch.rb +60 -0
  12. data/lib/concurrent/dataflow.rb +85 -0
  13. data/lib/concurrent/dereferenceable.rb +69 -31
  14. data/lib/concurrent/event.rb +27 -21
  15. data/lib/concurrent/future.rb +103 -43
  16. data/lib/concurrent/ivar.rb +78 -0
  17. data/lib/concurrent/mvar.rb +154 -0
  18. data/lib/concurrent/obligation.rb +94 -9
  19. data/lib/concurrent/postable.rb +11 -9
  20. data/lib/concurrent/promise.rb +101 -127
  21. data/lib/concurrent/safe_task_executor.rb +28 -0
  22. data/lib/concurrent/scheduled_task.rb +60 -54
  23. data/lib/concurrent/stoppable.rb +2 -2
  24. data/lib/concurrent/supervisor.rb +36 -29
  25. data/lib/concurrent/thread_local_var.rb +117 -0
  26. data/lib/concurrent/timer_task.rb +28 -30
  27. data/lib/concurrent/utilities.rb +1 -1
  28. data/lib/concurrent/version.rb +1 -1
  29. data/spec/concurrent/agent_spec.rb +121 -230
  30. data/spec/concurrent/atomic_spec.rb +201 -0
  31. data/spec/concurrent/condition_spec.rb +171 -0
  32. data/spec/concurrent/copy_on_notify_observer_set_spec.rb +10 -0
  33. data/spec/concurrent/copy_on_write_observer_set_spec.rb +10 -0
  34. data/spec/concurrent/count_down_latch_spec.rb +125 -0
  35. data/spec/concurrent/dataflow_spec.rb +160 -0
  36. data/spec/concurrent/dereferenceable_shared.rb +145 -0
  37. data/spec/concurrent/event_spec.rb +44 -9
  38. data/spec/concurrent/fixed_thread_pool_spec.rb +0 -1
  39. data/spec/concurrent/future_spec.rb +184 -69
  40. data/spec/concurrent/ivar_spec.rb +192 -0
  41. data/spec/concurrent/mvar_spec.rb +380 -0
  42. data/spec/concurrent/obligation_spec.rb +193 -0
  43. data/spec/concurrent/observer_set_shared.rb +233 -0
  44. data/spec/concurrent/postable_shared.rb +3 -7
  45. data/spec/concurrent/promise_spec.rb +270 -192
  46. data/spec/concurrent/safe_task_executor_spec.rb +58 -0
  47. data/spec/concurrent/scheduled_task_spec.rb +142 -38
  48. data/spec/concurrent/thread_local_var_spec.rb +113 -0
  49. data/spec/concurrent/thread_pool_shared.rb +2 -3
  50. data/spec/concurrent/timer_task_spec.rb +31 -1
  51. data/spec/spec_helper.rb +2 -3
  52. data/spec/support/functions.rb +4 -0
  53. data/spec/support/less_than_or_equal_to_matcher.rb +5 -0
  54. metadata +50 -30
  55. data/lib/concurrent/contract.rb +0 -21
  56. data/lib/concurrent/event_machine_defer_proxy.rb +0 -22
  57. data/md/actor.md +0 -404
  58. data/md/agent.md +0 -142
  59. data/md/channel.md +0 -40
  60. data/md/dereferenceable.md +0 -49
  61. data/md/future.md +0 -125
  62. data/md/obligation.md +0 -32
  63. data/md/promise.md +0 -217
  64. data/md/scheduled_task.md +0 -156
  65. data/md/supervisor.md +0 -246
  66. data/md/thread_pool.md +0 -225
  67. data/md/timer_task.md +0 -191
  68. data/spec/concurrent/contract_spec.rb +0 -34
  69. data/spec/concurrent/event_machine_defer_proxy_spec.rb +0 -240
@@ -0,0 +1,85 @@
1
+ require 'concurrent/atomic'
2
+ require 'concurrent/future'
3
+
4
+ module Concurrent
5
+
6
+ # @!visibility private
7
+ class DependencyCounter # :nodoc:
8
+
9
+ def initialize(count, &block)
10
+ @counter = AtomicFixnum.new(count)
11
+ @block = block
12
+ end
13
+
14
+ def update(time, value, reason)
15
+ if @counter.decrement == 0
16
+ @block.call
17
+ end
18
+ end
19
+ end
20
+
21
+ # Dataflow allows you to create a task that will be scheduled then all of its
22
+ # data dependencies are available. Data dependencies are +Future+ values. The
23
+ # dataflow task itself is also a +Future+ value, so you can build up a graph of
24
+ # these tasks, each of which is run when all the data and other tasks it depends
25
+ # on are available or completed.
26
+ #
27
+ # Our syntax is somewhat related to that of Akka's +flow+ and Habanero Java's
28
+ # +DataDrivenFuture+. However unlike Akka we don't schedule a task at all until
29
+ # it is ready to run, and unlike Habanero Java we pass the data values into the
30
+ # task instead of dereferencing them again in the task.
31
+ #
32
+ # The theory of dataflow goes back to the 80s. In the terminology of the literature,
33
+ # our implementation is coarse-grained, in that each task can be many instructions,
34
+ # and dynamic in that you can create more tasks within other tasks.
35
+ #
36
+ # @example Parallel Fibonacci calculator
37
+ # def fib(n)
38
+ # if n < 2
39
+ # Concurrent::dataflow { n }
40
+ # else
41
+ # n1 = fib(n - 1)
42
+ # n2 = fib(n - 2)
43
+ # Concurrent::dataflow(n1, n2) { |v1, v2| v1 + v2 }
44
+ # end
45
+ # end
46
+ #
47
+ # f = fib(14) #=> #<Concurrent::Future:0x000001019a26d8 ...
48
+ #
49
+ # # wait up to 1 second for the answer...
50
+ # f.value(1) #=> 377
51
+ #
52
+ # @param [Future] inputs zero or more +Future+ operations that this dataflow depends upon
53
+ #
54
+ # @yield The operation to perform once all the dependencies are met
55
+ # @yieldparam [Future] inputs each of the +Future+ inputs to the dataflow
56
+ # @yieldreturn [Object] the result of the block operation
57
+ #
58
+ # @return [Object] the result of all the operations
59
+ #
60
+ # @raise [ArgumentError] if no block is given
61
+ # @raise [ArgumentError] if any of the inputs are not +IVar+s
62
+ def dataflow(*inputs, &block)
63
+ raise ArgumentError.new('no block given') unless block_given?
64
+ raise ArgumentError.new('not all dependencies are IVars') unless inputs.all? { |input| input.is_a? IVar }
65
+
66
+ result = Future.new do
67
+ values = inputs.map { |input| input.value }
68
+ block.call(*values)
69
+ end
70
+
71
+ if inputs.empty?
72
+ result.execute
73
+ else
74
+ counter = DependencyCounter.new(inputs.size) { result.execute }
75
+
76
+ inputs.each do |input|
77
+ input.add_observer counter
78
+ end
79
+ end
80
+
81
+ result
82
+ end
83
+
84
+ module_function :dataflow
85
+ end
@@ -1,24 +1,76 @@
1
1
  module Concurrent
2
2
 
3
3
  # Object references in Ruby are mutable. This can lead to serious problems when
4
- # the `#value` of a concurrent object is a mutable reference. Which is always the
5
- # case unless the value is a `Fixnum`, `Symbol`, or similar "primitive" data type.
6
- # Most classes in this library that expose a `#value` getter method do so using
4
+ # the +#value+ of a concurrent object is a mutable reference. Which is always the
5
+ # case unless the value is a +Fixnum+, +Symbol+, or similar "primitive" data type.
6
+ # Most classes in this library that expose a +#value+ getter method do so using
7
7
  # this mixin module.
8
8
  module Dereferenceable
9
9
 
10
+ # Return the value this object represents after applying the options specified
11
+ # by the +#set_deref_options+ method.
12
+ #
13
+ # When multiple deref options are set the order of operations is strictly defined.
14
+ # The order of deref operations is:
15
+ # * +:copy_on_deref+
16
+ # * +:dup_on_deref+
17
+ # * +:freeze_on_deref+
18
+ #
19
+ # Because of this ordering there is no need to +#freeze+ an object created by a
20
+ # provided +:copy_on_deref+ block. Simply set +:freeze_on_deref+ to +true+.
21
+ # Setting both +:dup_on_deref+ to +true+ and +:freeze_on_deref+ to +true is
22
+ # as close to the behavior of a "pure" functional language (like Erlang, Clojure,
23
+ # or Haskell) as we are likely to get in Ruby.
24
+ #
25
+ # This method is thread-safe and synchronized with the internal +#mutex+.
26
+ #
27
+ # @return [Object] the current value of the object
28
+ def value
29
+ mutex.synchronize do
30
+ apply_deref_options(@value)
31
+ end
32
+ end
33
+ alias_method :deref, :value
34
+
35
+ protected
36
+
37
+ # A mutex lock used for synchronizing thread-safe operations. Methods defined
38
+ # by +Dereferenceable+ are synchronized using the +Mutex+ returned from this
39
+ # method. Operations performed by the including class that operate on the
40
+ # +@value+ instance variable should be locked with this +Mutex+.
41
+ #
42
+ # @return [Mutex] the synchronization object
43
+ #
44
+ # @!visibility public
45
+ def mutex
46
+ @mutex
47
+ end
48
+
49
+ # Initializes the internal +Mutex+.
50
+ #
51
+ # @note This method *must* be called from within the constructor of the including class.
52
+ #
53
+ # @see #mutex
54
+ #
55
+ # @!visibility public
56
+ def init_mutex
57
+ @mutex = Mutex.new
58
+ end
59
+
10
60
  # Set the options which define the operations #value performs before
11
61
  # returning data to the caller (dereferencing).
12
62
  #
13
- # @note Many classes that include this module will call #set_deref_options
63
+ # @note Most classes that include this module will call +#set_deref_options+
14
64
  # from within the constructor, thus allowing these options to be set at
15
65
  # object creation.
16
66
  #
17
67
  # @param [Hash] opts the options defining dereference behavior.
18
- # @option opts [String] :dup_on_deref Call #dup before returning the data (default: false)
19
- # @option opts [String] :freeze_on_deref Call #freeze before returning the data (default: false)
20
- # @option opts [String] :copy_on_deref Call the given `Proc` passing the internal value and
21
- # returning the value returned from the proc (default: `nil`)
68
+ # @option opts [String] :dup_on_deref (false) call +#dup+ before returning the data
69
+ # @option opts [String] :freeze_on_deref (false) call +#freeze+ before returning the data
70
+ # @option opts [String] :copy_on_deref (nil) call the given +Proc+ passing the internal value and
71
+ # returning the value returned from the proc
72
+ #
73
+ # @!visibility public
22
74
  def set_deref_options(opts = {})
23
75
  mutex.synchronize do
24
76
  @dup_on_deref = opts[:dup_on_deref] || opts[:dup]
@@ -28,29 +80,15 @@ module Concurrent
28
80
  end
29
81
  end
30
82
 
31
- # Return the value this object represents after applying the options specified
32
- # by the #set_deref_options method.
33
- def value
34
- return nil if @value.nil?
35
- return @value if @do_nothing_on_deref
36
- mutex.synchronize do
37
- value = @value
38
- value = @copy_on_deref.call(value) if @copy_on_deref
39
- value = value.dup if @dup_on_deref
40
- value = value.freeze if @freeze_on_deref
41
- value
42
- end
43
- end
44
- alias_method :deref, :value
45
-
46
- protected
47
-
48
- def mutex # :nodoc:
49
- @mutex
50
- end
51
-
52
- def init_mutex
53
- @mutex = Mutex.new
83
+ # @!visibility private
84
+ def apply_deref_options(value) # :nodoc:
85
+ return nil if value.nil?
86
+ return value if @do_nothing_on_deref
87
+ value = value
88
+ value = @copy_on_deref.call(value) if @copy_on_deref
89
+ value = value.dup if @dup_on_deref
90
+ value = value.freeze if @freeze_on_deref
91
+ value
54
92
  end
55
93
  end
56
94
  end
@@ -1,41 +1,42 @@
1
1
  require 'thread'
2
2
  require 'concurrent/utilities'
3
+ require 'concurrent/condition'
3
4
 
4
5
  module Concurrent
5
6
 
6
7
  # Old school kernel-style event reminiscent of Win32 programming in C++.
7
8
  #
8
- # When an `Event` is created it is in the `unset` state. Threads can choose to
9
- # `#wait` on the event, blocking until released by another thread. When one
10
- # thread wants to alert all blocking threads it calls the `#set` method which
11
- # will then wake up all listeners. Once an `Event` has been set it remains set.
12
- # New threads calling `#wait` will return immediately. An `Event` may be
13
- # `#reset` at any time once it has been set.
9
+ # When an +Event+ is created it is in the +unset+ state. Threads can choose to
10
+ # +#wait+ on the event, blocking until released by another thread. When one
11
+ # thread wants to alert all blocking threads it calls the +#set+ method which
12
+ # will then wake up all listeners. Once an +Event+ has been set it remains set.
13
+ # New threads calling +#wait+ will return immediately. An +Event+ may be
14
+ # +#reset+ at any time once it has been set.
14
15
  #
15
16
  # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682655.aspx
16
17
  class Event
17
18
 
18
- # Creates a new `Event` in the unset state. Threads calling `#wait` on the
19
- # `Event` will block.
19
+ # Creates a new +Event+ in the unset state. Threads calling +#wait+ on the
20
+ # +Event+ will block.
20
21
  def initialize
21
22
  @set = false
22
23
  @mutex = Mutex.new
23
- @condition = ConditionVariable.new
24
+ @condition = Condition.new
24
25
  end
25
26
 
26
27
  # Is the object in the set state?
27
28
  #
28
- # @return [Boolean] indicating whether or not the `Event` has been set
29
+ # @return [Boolean] indicating whether or not the +Event+ has been set
29
30
  def set?
30
31
  @mutex.synchronize do
31
32
  @set
32
33
  end
33
34
  end
34
35
 
35
- # Trigger the event, setting the state to `set` and releasing all threads
36
- # waiting on the event. Has no effect if the `Event` has already been set.
36
+ # Trigger the event, setting the state to +set+ and releasing all threads
37
+ # waiting on the event. Has no effect if the +Event+ has already been set.
37
38
  #
38
- # @return [Boolean] should always return `true`
39
+ # @return [Boolean] should always return +true+
39
40
  def set
40
41
  @mutex.synchronize do
41
42
  return true if @set
@@ -46,10 +47,10 @@ module Concurrent
46
47
  true
47
48
  end
48
49
 
49
- # Reset a previously set event back to the `unset` state.
50
- # Has no effect if the `Event` has not yet been set.
50
+ # Reset a previously set event back to the +unset+ state.
51
+ # Has no effect if the +Event+ has not yet been set.
51
52
  #
52
- # @return [Boolean] should always return `true`
53
+ # @return [Boolean] should always return +true+
53
54
  def reset
54
55
  @mutex.synchronize do
55
56
  @set = false
@@ -58,15 +59,20 @@ module Concurrent
58
59
  true
59
60
  end
60
61
 
61
- # Wait a given number of seconds for the `Event` to be set by another
62
- # thread. Will wait forever when no `timeout` value is given. Returns
63
- # immediately if the `Event` has already been set.
62
+ # Wait a given number of seconds for the +Event+ to be set by another
63
+ # thread. Will wait forever when no +timeout+ value is given. Returns
64
+ # immediately if the +Event+ has already been set.
64
65
  #
65
- # @return [Boolean] true if the `Event` was set before timeout else false
66
+ # @return [Boolean] true if the +Event+ was set before timeout else false
66
67
  def wait(timeout = nil)
67
68
  @mutex.synchronize do
68
69
  return true if @set
69
- @condition.wait(@mutex, timeout)
70
+
71
+ remaining = Condition::Result.new(timeout)
72
+ while !@set && remaining.can_wait?
73
+ remaining = @condition.wait(@mutex, remaining.remaining_time)
74
+ end
75
+
70
76
  @set
71
77
  end
72
78
  end
@@ -1,62 +1,122 @@
1
1
  require 'thread'
2
- require 'observer'
3
2
 
4
- require 'concurrent/global_thread_pool'
5
3
  require 'concurrent/obligation'
4
+ require 'concurrent/global_thread_pool'
5
+ require 'concurrent/safe_task_executor'
6
6
 
7
7
  module Concurrent
8
8
 
9
- class Future
9
+ # A +Future+ represents a promise to complete an action at some time in the future.
10
+ # The action is atomic and permanent. The idea behind a future is to send an operation
11
+ # for asynchronous completion, do other stuff, then return and retrieve the result
12
+ # of the async operation at a later time.
13
+ #
14
+ # A +Future+ has four possible states: *:unscheduled*, *:pending*, *:rejected*, or *:fulfilled*.
15
+ # When a +Future+ is created its state is set to *:unscheduled*. Once the +#execute+ method is
16
+ # called the state becomes *:pending* and will remain in that state until processing is
17
+ # complete. A completed +Future+ is either *:rejected*, indicating that an exception was
18
+ # thrown during processing, or *:fulfilled*, indicating success. If a +Future+ is *:fulfilled*
19
+ # its +value+ will be updated to reflect the result of the operation. If *:rejected* the
20
+ # +reason+ will be updated with a reference to the thrown exception. The predicate methods
21
+ # +#unscheduled?+, +#pending?+, +#rejected?+, and +fulfilled?+ can be called at any time to
22
+ # obtain the state of the +Future+, as can the +#state+ method, which returns a symbol.
23
+ #
24
+ # Retrieving the value of a +Future+ is done through the +#value+ (alias: +#deref+) method.
25
+ # Obtaining the value of a +Future+ is a potentially blocking operation. When a +Future+ is
26
+ # *:rejected* a call to +#value+ will return +nil+ immediately. When a +Future+ is
27
+ # *:fulfilled* a call to +#value+ will immediately return the current value. When a
28
+ # +Future+ is *:pending* a call to +#value+ will block until the +Future+ is either
29
+ # *:rejected* or *:fulfilled*. A *timeout* value can be passed to +#value+ to limit how
30
+ # long the call will block. If +nil+ the call will block indefinitely. If +0+ the call will
31
+ # not block. Any other integer or float value will indicate the maximum number of seconds to block.
32
+ #
33
+ # The +Future+ class also includes the behavior of the Ruby standard library +Observable+ module,
34
+ # but does so in a thread-safe way. On fulfillment or rejection all observers will be notified
35
+ # according to the normal +Observable+ behavior. The observer callback function will be called
36
+ # with three parameters: the +Time+ of fulfillment/rejection, the final +value+, and the final
37
+ # +reason+. Observers added after fulfillment/rejection will still be notified as normal.
38
+ #
39
+ # @see http://ruby-doc.org/stdlib-2.1.1/libdoc/observer/rdoc/Observable.html Ruby Observable module
40
+ # @see http://clojuredocs.org/clojure_core/clojure.core/future Clojure's future function
41
+ # @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/Future.html java.util.concurrent.Future
42
+ class Future < IVar
10
43
  include Obligation
11
- include Observable
12
44
  include UsesGlobalThreadPool
13
45
 
14
- def initialize(*args, &block)
15
- init_mutex
16
- unless block_given?
17
- @state = :fulfilled
18
- else
19
- @value = nil
20
- @state = :pending
21
- Future.thread_pool.post(*args) do
22
- work(*args, &block)
23
- end
24
- end
46
+ # Create a new +Future+ in the +:unscheduled+ state.
47
+ #
48
+ # @yield the asynchronous operation to perform
49
+ #
50
+ # @param [Hash] opts the options to create a message with
51
+ # @option opts [String] :dup_on_deref (false) call +#dup+ before returning the data
52
+ # @option opts [String] :freeze_on_deref (false) call +#freeze+ before returning the data
53
+ # @option opts [String] :copy_on_deref (nil) call the given +Proc+ passing the internal value and
54
+ # returning the value returned from the proc
55
+ #
56
+ # @raise [ArgumentError] if no block is given
57
+ def initialize(opts = {}, &block)
58
+ raise ArgumentError.new('no block given') unless block_given?
59
+ super(IVar::NO_VALUE, opts)
60
+ @state = :unscheduled
61
+ @task = block
25
62
  end
26
63
 
27
- def add_observer(observer, func = :update)
28
- val = self.value
29
- mutex.synchronize do
30
- if event.set?
31
- Future.thread_pool.post(func, Time.now, val, @reason) do |f, *args|
32
- observer.send(f, *args)
33
- end
34
- else
35
- super
36
- end
64
+ # Execute an +:unscheduled+ +Future+. Immediately sets the state to +:pending+ and
65
+ # passes the block to a new thread/thread pool for eventual execution.
66
+ # Does nothing if the +Future+ is in any state other than +:unscheduled+.
67
+ #
68
+ # @return [Future] a reference to +self+
69
+ #
70
+ # @example Instance and execute in separate steps
71
+ # future = Concurrent::Future.new{ sleep(1); 42 }
72
+ # future.state #=> :unscheduled
73
+ # future.execute
74
+ # future.state #=> :pending
75
+ #
76
+ # @example Instance and execute in one line
77
+ # future = Concurrent::Future.new{ sleep(1); 42 }.execute
78
+ # future.state #=> :pending
79
+ #
80
+ # @since 0.5.0
81
+ def execute
82
+ if compare_and_set_state(:pending, :unscheduled)
83
+ Future.thread_pool.post { work }
84
+ self
37
85
  end
38
- return func
39
86
  end
40
87
 
88
+ # Create a new +Future+ object with the given block, execute it, and return the
89
+ # +:pending+ object.
90
+ #
91
+ # @yield the asynchronous operation to perform
92
+ #
93
+ # @option opts [String] :dup_on_deref (false) call +#dup+ before returning the data
94
+ # @option opts [String] :freeze_on_deref (false) call +#freeze+ before returning the data
95
+ # @option opts [String] :copy_on_deref (nil) call the given +Proc+ passing the internal value and
96
+ # returning the value returned from the proc
97
+ #
98
+ # @return [Future] the newly created +Future+ in the +:pending+ state
99
+ #
100
+ # @raise [ArgumentError] if no block is given
101
+ #
102
+ # @example
103
+ # future = Concurrent::Future.execute{ sleep(1); 42 }
104
+ # future.state #=> :pending
105
+ #
106
+ # @since 0.5.0
107
+ def self.execute(opts = {}, &block)
108
+ return Future.new(opts, &block).execute
109
+ end
110
+
111
+ protected :set, :fail, :complete
112
+
41
113
  private
42
114
 
43
- # @private
44
- def work(*args) # :nodoc:
45
- begin
46
- @value = yield(*args)
47
- @state = :fulfilled
48
- rescue Exception => ex
49
- @reason = ex
50
- @state = :rejected
51
- ensure
52
- val = self.value
53
- mutex.synchronize do
54
- event.set
55
- changed
56
- notify_observers(Time.now, val, @reason)
57
- delete_observers
58
- end
59
- end
115
+ # @!visibility private
116
+ def work # :nodoc:
117
+ success, val, reason = SafeTaskExecutor.new(@task).execute
118
+ complete(success, val, reason)
60
119
  end
120
+
61
121
  end
62
122
  end