concurrent-ruby 0.4.1 → 0.5.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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