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,125 @@
1
+ module Concurrent
2
+
3
+ # @!visibility private
4
+ module MutexAtomicFixnum # :nodoc:
5
+
6
+ # @!visibility private
7
+ def allocate_storage(init) # :nodoc:
8
+ @value = init
9
+ @mutex = Mutex.new
10
+ end
11
+
12
+ def value
13
+ @mutex.synchronize do
14
+ @value
15
+ end
16
+ end
17
+
18
+ def value=(value)
19
+ raise ArgumentError.new('value must be a Fixnum') unless value.is_a?(Fixnum)
20
+ @mutex.synchronize do
21
+ @value = value
22
+ end
23
+ end
24
+
25
+ def increment
26
+ @mutex.synchronize do
27
+ @value += 1
28
+ end
29
+ end
30
+
31
+ def decrement
32
+ @mutex.synchronize do
33
+ @value -= 1
34
+ end
35
+ end
36
+
37
+ # @!visibility private
38
+ def compare_and_set(expect, update) # :nodoc:
39
+ @mutex.synchronize do
40
+ if @value == expect
41
+ @value = update
42
+ true
43
+ else
44
+ false
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # @!visibility private
51
+ module JavaAtomicFixnum # :nodoc:
52
+
53
+ # @!visibility private
54
+ def allocate_storage(init) # :nodoc:
55
+ @atomic = java.util.concurrent.atomic.AtomicLong.new(init)
56
+ end
57
+
58
+ def value
59
+ @atomic.get
60
+ end
61
+
62
+ def value=(value)
63
+ raise ArgumentError.new('value must be a Fixnum') unless value.is_a?(Fixnum)
64
+ @atomic.set(value)
65
+ end
66
+
67
+ def increment
68
+ @atomic.increment_and_get
69
+ end
70
+
71
+ def decrement
72
+ @atomic.decrement_and_get
73
+ end
74
+
75
+ # @!visibility private
76
+ def compare_and_set(expect, update) # :nodoc:
77
+ @atomic.compare_and_set(expect, update)
78
+ end
79
+ end
80
+
81
+ # A numeric value that can be updated atomically. Reads and writes to an atomic
82
+ # fixnum and thread-safe and guaranteed to succeed. Reads and writes may block
83
+ # briefly but no explicit locking is required.
84
+ #
85
+ # @!method value()
86
+ # Retrieves the current +Fixnum+ value
87
+ # @return [Fixnum] the current value
88
+ #
89
+ # @!method value=(value)
90
+ # Explicitly sets the value
91
+ # @param [Fixnum] value the new value to be set
92
+ # @return [Fixnum] the current value
93
+ # @raise [ArgumentError] if the new value is not a +Fixnum+
94
+ #
95
+ # @!method increment()
96
+ # Increases the current value by 1
97
+ # @return [Fixnum] the current value after incrementation
98
+ #
99
+ # @!method decrement()
100
+ # Decreases the current value by 1
101
+ # @return [Fixnum] the current value after decrementation
102
+ #
103
+ # @since 0.5.0
104
+ # @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicLong.html java.util.concurrent.atomic.AtomicLong
105
+ class AtomicFixnum
106
+
107
+ # Creates a new +AtomicFixnum+ with the given initial value.
108
+ #
109
+ # @param [Fixnum] init the initial value
110
+ # @raise [ArgumentError] if the initial value is not a +Fixnum+
111
+ def initialize(init = 0)
112
+ raise ArgumentError.new('initial value must be a Fixnum') unless init.is_a?(Fixnum)
113
+ allocate_storage(init)
114
+ end
115
+
116
+ if defined? java.util
117
+ include JavaAtomicFixnum
118
+ else
119
+ include MutexAtomicFixnum
120
+ end
121
+
122
+ alias_method :up, :increment
123
+ alias_method :down, :decrement
124
+ end
125
+ end
@@ -3,9 +3,44 @@ require 'concurrent/stoppable'
3
3
 
4
4
  module Concurrent
5
5
 
6
+ # +Channel+ is a functional programming variation of +Actor+, based very loosely on the
7
+ # *MailboxProcessor* agent in F#. +Actor+ is used to create objects that receive messages
8
+ # from other threads then processes those messages based on the behavior of the class.
9
+ # +Channel+ creates objects that receive messages and processe them using the block
10
+ # given at construction. +Channel+ is implemented as a subclass of +Actor+ and supports
11
+ # all message-passing methods of that class. +Channel+ also supports pools with a shared
12
+ # mailbox.
13
+ #
14
+ # @example Basic usage
15
+ # channel = Concurrent::Channel.new do |msg|
16
+ # sleep(1)
17
+ # puts "#{msg}\n"
18
+ # end
19
+ #
20
+ # channel.run! => #<Thread:0x007fa123d95fc8 sleep>
21
+ #
22
+ # channel.post("Hello, World!") => 1
23
+ # # wait...
24
+ # => Hello, World!
25
+ #
26
+ # future = channel.post? "Don't Panic." => #<Concurrent::IVar:0x007fa123d6d9d8 @state=:pending...
27
+ # future.pending? => true
28
+ # # wait...
29
+ # => "Don't Panic."
30
+ # future.fulfilled? => true
31
+ #
32
+ # channel.stop => true
33
+ #
34
+ # @see http://blogs.msdn.com/b/dsyme/archive/2010/02/15/async-and-parallel-design-patterns-in-f-part-3-agents.aspx Async and Parallel Design Patterns in F#: Agents
35
+ # @see http://msdn.microsoft.com/en-us/library/ee370357.aspx Control.MailboxProcessor<'Msg> Class (F#)
6
36
  class Channel < Actor
7
37
  include Stoppable
8
38
 
39
+ # Initialize a new object with a block operation to be performed in response
40
+ # to every received message.
41
+ #
42
+ # @yield [message] Removes the next message from the queue and processes it
43
+ # @yieldparam [Array] msg The next message post to the channel
9
44
  def initialize(&block)
10
45
  raise ArgumentError.new('no block given') unless block_given?
11
46
  super()
@@ -21,7 +56,7 @@ module Concurrent
21
56
 
22
57
  private
23
58
 
24
- def act(*message)
59
+ def act(*message) # :nodoc:
25
60
  return @task.call(*message)
26
61
  end
27
62
  end
@@ -0,0 +1,67 @@
1
+ module Concurrent
2
+
3
+ # Condition is a better implementation of standard Ruby ConditionVariable.
4
+ # The biggest difference is the wait return value: Condition#wait returns
5
+ # Condition::Result which make possible to know if waiting thread has been woken up
6
+ # by an another thread (using #signal or #broadcast) or due to timeout.
7
+ #
8
+ # Every #wait must be guarded by a locked Mutex or a ThreadError will be risen.
9
+ # Although it's not mandatory, it's recommended to call also #signal and #broadcast within
10
+ # the same mutex
11
+ class Condition
12
+
13
+ class Result
14
+ def initialize(remaining_time)
15
+ @remaining_time = remaining_time
16
+ end
17
+
18
+ attr_reader :remaining_time
19
+
20
+ # @return [Boolean] true if current thread has been waken up by a #signal or a #broadcast call, otherwise false
21
+ def woken_up?
22
+ @remaining_time.nil? || @remaining_time > 0
23
+ end
24
+
25
+ # @return [Boolean] true if current thread has been waken up due to a timeout, otherwise false
26
+ def timed_out?
27
+ @remaining_time != nil && @remaining_time <= 0
28
+ end
29
+
30
+ alias_method :can_wait?, :woken_up?
31
+
32
+ end
33
+
34
+ def initialize
35
+ @condition = ConditionVariable.new
36
+ end
37
+
38
+ # @param [Mutex] mutex the locked mutex guarding the wait
39
+ # @param [Object] timeout nil means no timeout
40
+ # @return [Result]
41
+ def wait(mutex, timeout = nil)
42
+ start_time = Time.now.to_f
43
+ @condition.wait(mutex, timeout)
44
+
45
+ if timeout.nil?
46
+ Result.new(nil)
47
+ else
48
+ Result.new(start_time + timeout - Time.now.to_f)
49
+ end
50
+ end
51
+
52
+ # Wakes up a waiting thread
53
+ # @return [true]
54
+ def signal
55
+ @condition.signal
56
+ true
57
+ end
58
+
59
+ # Wakes up all waiting threads
60
+ # @return [true]
61
+ def broadcast
62
+ @condition.broadcast
63
+ true
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,80 @@
1
+ module Concurrent
2
+
3
+ # A thread safe observer set implemented using copy-on-read approach:
4
+ # observers are added and removed from a thread safe collection; every time
5
+ # a notification is required the internal data structure is copied to
6
+ # prevent concurrency issues
7
+ class CopyOnNotifyObserverSet
8
+
9
+ def initialize
10
+ @mutex = Mutex.new
11
+ @observers = {}
12
+ end
13
+
14
+ # Adds an observer to this set
15
+ # @param [Object] observer the observer to add
16
+ # @param [Symbol] func the function to call on the observer during notification. Default is :update
17
+ # @return [Symbol] the added function
18
+ def add_observer(observer, func=:update)
19
+ @mutex.synchronize { @observers[observer] = func }
20
+ end
21
+
22
+ # @param [Object] observer the observer to remove
23
+ # @return [Object] the deleted observer
24
+ def delete_observer(observer)
25
+ @mutex.synchronize { @observers.delete(observer) }
26
+ observer
27
+ end
28
+
29
+ # Deletes all observers
30
+ # @return [CopyOnWriteObserverSet] self
31
+ def delete_observers
32
+ @mutex.synchronize { @observers.clear }
33
+ self
34
+ end
35
+
36
+ # @return [Integer] the observers count
37
+ def count_observers
38
+ @mutex.synchronize { @observers.count }
39
+ end
40
+
41
+ # Notifies all registered observers with optional args
42
+ # @param [Object] args arguments to be passed to each observer
43
+ # @return [CopyOnWriteObserverSet] self
44
+ def notify_observers(*args, &block)
45
+ observers = @mutex.synchronize { @observers.dup }
46
+ notify_to(observers, *args, &block)
47
+
48
+ self
49
+ end
50
+
51
+ # Notifies all registered observers with optional args and deletes them.
52
+ #
53
+ # @param [Object] args arguments to be passed to each observer
54
+ # @return [CopyOnWriteObserverSet] self
55
+ def notify_and_delete_observers(*args, &block)
56
+ observers = duplicate_and_clear_observers
57
+ notify_to(observers, *args, &block)
58
+
59
+ self
60
+ end
61
+
62
+ private
63
+
64
+ def duplicate_and_clear_observers
65
+ @mutex.synchronize do
66
+ observers = @observers.dup
67
+ @observers.clear
68
+ observers
69
+ end
70
+ end
71
+
72
+ def notify_to(observers, *args)
73
+ raise ArgumentError.new('cannot give arguments and a block') if block_given? && ! args.empty?
74
+ observers.each do |observer, function|
75
+ args = yield if block_given?
76
+ observer.send(function, *args)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,94 @@
1
+ module Concurrent
2
+
3
+ # A thread safe observer set implemented using copy-on-write approach:
4
+ # every time an observer is added or removed the whole internal data structure is
5
+ # duplicated and replaced with a new one.
6
+ class CopyOnWriteObserverSet
7
+
8
+ def initialize
9
+ @mutex = Mutex.new
10
+ @observers = {}
11
+ end
12
+
13
+ # Adds an observer to this set
14
+ # @param [Object] observer the observer to add
15
+ # @param [Symbol] func the function to call on the observer during notification. Default is :update
16
+ # @return [Symbol] the added function
17
+ def add_observer(observer, func=:update)
18
+ @mutex.synchronize do
19
+ new_observers = @observers.dup
20
+ new_observers[observer] = func
21
+ @observers = new_observers
22
+ end
23
+ func
24
+ end
25
+
26
+ # @param [Object] observer the observer to remove
27
+ # @return [Object] the deleted observer
28
+ def delete_observer(observer)
29
+ @mutex.synchronize do
30
+ new_observers = @observers.dup
31
+ new_observers.delete(observer)
32
+ @observers = new_observers
33
+ end
34
+ observer
35
+ end
36
+
37
+ # Deletes all observers
38
+ # @return [CopyOnWriteObserverSet] self
39
+ def delete_observers
40
+ self.observers = {}
41
+ self
42
+ end
43
+
44
+
45
+ # @return [Integer] the observers count
46
+ def count_observers
47
+ observers.count
48
+ end
49
+
50
+ # Notifies all registered observers with optional args
51
+ # @param [Object] args arguments to be passed to each observer
52
+ # @return [CopyOnWriteObserverSet] self
53
+ def notify_observers(*args, &block)
54
+ notify_to(observers, *args, &block)
55
+ self
56
+ end
57
+
58
+ # Notifies all registered observers with optional args and deletes them.
59
+ #
60
+ # @param [Object] args arguments to be passed to each observer
61
+ # @return [CopyOnWriteObserverSet] self
62
+ def notify_and_delete_observers(*args, &block)
63
+ old = clear_observers_and_return_old
64
+ notify_to(old, *args, &block)
65
+ self
66
+ end
67
+
68
+ private
69
+
70
+ def notify_to(observers, *args)
71
+ raise ArgumentError.new('cannot give arguments and a block') if block_given? && ! args.empty?
72
+ observers.each do |observer, function|
73
+ args = yield if block_given?
74
+ observer.send(function, *args)
75
+ end
76
+ end
77
+
78
+ def observers
79
+ @mutex.synchronize { @observers }
80
+ end
81
+
82
+ def observers=(new_set)
83
+ @mutex.synchronize { @observers = new_set}
84
+ end
85
+
86
+ def clear_observers_and_return_old
87
+ @mutex.synchronize do
88
+ old_observers = @observers
89
+ @observers = {}
90
+ old_observers
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,60 @@
1
+ module Concurrent
2
+
3
+ # A synchronization object that allows one thread to wait on multiple other threads.
4
+ # The thread that will wait creates a +CountDownLatch+ and sets the initial value
5
+ # (normally equal to the number of other threads). The initiating thread passes the
6
+ # latch to the other threads then waits for the other threads by calling the +#wait+
7
+ # method. Each of the other threads calls +#count_down+ when done with its work.
8
+ # When the latch counter reaches zero the waiting thread is unblocked and continues
9
+ # with its work. A +CountDownLatch+ can be used only once. Its value cannot be reset.
10
+ class CountDownLatch
11
+
12
+ # Create a new +CountDownLatch+ with the initial +count+.
13
+ #
14
+ # @param [Fixnum] count the initial count
15
+ #
16
+ # @raise [ArgumentError] if +count+ is not an integer or is less than zero
17
+ def initialize(count)
18
+ unless count.is_a?(Fixnum) && count >= 0
19
+ raise ArgumentError.new('count must be in integer greater than or equal zero')
20
+ end
21
+ @mutex = Mutex.new
22
+ @condition = Condition.new
23
+ @count = count
24
+ end
25
+
26
+ # Block on the latch until the counter reaches zero or until +timeout+ is reached.
27
+ #
28
+ # @param [Fixnum] timeout the number of seconds to wait for the counter or +nil+
29
+ # to block indefinitely
30
+ # @return [Boolean] +true+ if the +count+ reaches zero else false on +timeout+
31
+ def wait(timeout = nil)
32
+ @mutex.synchronize do
33
+
34
+ remaining = Condition::Result.new(timeout)
35
+ while @count > 0 && remaining.can_wait?
36
+ remaining = @condition.wait(@mutex, remaining.remaining_time)
37
+ end
38
+
39
+ @count == 0
40
+ end
41
+ end
42
+
43
+ # Signal the latch to decrement the counter. Will signal all blocked threads when
44
+ # the +count+ reaches zero.
45
+ def count_down
46
+ @mutex.synchronize do
47
+ @count -= 1 if @count > 0
48
+ @condition.broadcast if @count == 0
49
+ end
50
+ end
51
+
52
+ # The current value of the counter.
53
+ #
54
+ # @return [Fixnum] the current value of the counter
55
+ def count
56
+ @mutex.synchronize { @count }
57
+ end
58
+
59
+ end
60
+ end