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