quack_concurrency 0.5.4 → 0.6.0

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/quack_concurrency.rb +6 -13
  4. data/lib/quack_concurrency/condition_variable.rb +91 -85
  5. data/lib/quack_concurrency/condition_variable/waitable.rb +108 -0
  6. data/lib/quack_concurrency/error.rb +1 -1
  7. data/lib/quack_concurrency/future.rb +31 -30
  8. data/lib/quack_concurrency/future/canceled.rb +1 -0
  9. data/lib/quack_concurrency/future/complete.rb +1 -0
  10. data/lib/quack_concurrency/mutex.rb +140 -38
  11. data/lib/quack_concurrency/queue.rb +32 -28
  12. data/lib/quack_concurrency/reentrant_mutex.rb +64 -76
  13. data/lib/quack_concurrency/safe_condition_variable.rb +23 -0
  14. data/lib/quack_concurrency/safe_condition_variable/waitable.rb +21 -0
  15. data/lib/quack_concurrency/safe_sleeper.rb +80 -0
  16. data/lib/quack_concurrency/sleeper.rb +100 -0
  17. data/lib/quack_concurrency/waiter.rb +32 -23
  18. data/spec/condition_variable_spec.rb +216 -0
  19. data/spec/future_spec.rb +145 -79
  20. data/spec/mutex_spec.rb +441 -0
  21. data/spec/queue_spec.rb +217 -77
  22. data/spec/reentrant_mutex_spec.rb +394 -99
  23. data/spec/safe_condition_variable_spec.rb +115 -0
  24. data/spec/safe_sleeper_spec.rb +197 -0
  25. data/spec/sleeper.rb +197 -0
  26. data/spec/waiter_spec.rb +181 -0
  27. metadata +16 -14
  28. data/lib/quack_concurrency/queue/error.rb +0 -6
  29. data/lib/quack_concurrency/reentrant_mutex/error.rb +0 -6
  30. data/lib/quack_concurrency/semaphore.rb +0 -139
  31. data/lib/quack_concurrency/semaphore/error.rb +0 -6
  32. data/lib/quack_concurrency/uninterruptible_condition_variable.rb +0 -94
  33. data/lib/quack_concurrency/uninterruptible_sleeper.rb +0 -81
  34. data/lib/quack_concurrency/yielder.rb +0 -35
  35. data/spec/semaphore_spec.rb +0 -244
@@ -1,6 +1,7 @@
1
1
  module QuackConcurrency
2
2
  class Future
3
3
  class Canceled < Error
4
+
4
5
  end
5
6
  end
6
7
  end
@@ -1,6 +1,7 @@
1
1
  module QuackConcurrency
2
2
  class Future
3
3
  class Complete < Error
4
+
4
5
  end
5
6
  end
6
7
  end
@@ -1,26 +1,31 @@
1
1
  module QuackConcurrency
2
-
3
- # @note duck type for `::Thread::Mutex`
2
+
3
+ # {Mutex} is similar to +::Mutex+.
4
+ #
5
+ # A few differences include:
6
+ # * {#lock} supports passing a block and behaves like +::Mutex#synchronize+
7
+ # * {#unlock} supports passing a block
4
8
  class Mutex
5
-
9
+
6
10
  # Creates a new {Mutex} concurrency tool.
7
11
  # @return [Mutex]
8
12
  def initialize
13
+ @condition_variable = SafeConditionVariable.new
9
14
  @mutex = ::Mutex.new
10
- @condition_variable = UninterruptibleConditionVariable.new
11
15
  @owner = nil
12
16
  end
13
-
14
- # @raise [ThreadError] if current `Thread` is already locking it
17
+
18
+ # @raise [ThreadError] if current thread is already locking it
15
19
  #@overload lock
16
- # Obtains the lock or sleeps the current `Thread` until it is available.
20
+ # Obtains the lock or sleeps the current thread until it is available.
17
21
  # @return [void]
18
22
  #@overload lock(&block)
19
23
  # Obtains the lock, runs the block, then releases the lock when the block completes.
20
- # @yield block to run with the lock
24
+ # @raise [Exception] any exception raised in block
25
+ # @yield block to run while holding the lock
21
26
  # @return [Object] result of the block
22
27
  def lock(&block)
23
- raise ThreadError, 'this Thread is already locking this Mutex' if owned?
28
+ raise ThreadError, 'Attempt to lock a mutex which is already locked by this thread' if owned?
24
29
  if block_given?
25
30
  lock
26
31
  begin
@@ -36,40 +41,63 @@ module QuackConcurrency
36
41
  nil
37
42
  end
38
43
  end
39
-
44
+
45
+ # Checks if it is locked by a thread.
46
+ # @return [Boolean]
40
47
  def locked?
41
48
  !!@owner
42
49
  end
43
-
50
+
51
+ # Checks if it is locked by another thread.
52
+ # @return [Boolean]
53
+ def locked_out?
54
+ # don't need a mutex because we know #owned? can't change during the call
55
+ locked? && !owned?
56
+ end
57
+
58
+ # Checks if it is locked by current thread.
59
+ # @return [Boolean]
44
60
  def owned?
45
61
  @owner == caller
46
62
  end
47
-
63
+
64
+ # Returns the thread locking it if one exists.
65
+ # @return [nil,Thread] the locking +Thread+ if one exists, otherwise +nil+
48
66
  def owner
49
67
  @owner
50
68
  end
51
-
69
+
70
+ # Releases the lock and puts this thread to sleep.
71
+ # @param timeout [nil, Numeric] time to sleep in seconds or +nil+ to sleep forever
72
+ # @raise [TypeError] if +timeout+ is not +nil+ or +Numeric+
73
+ # @raise [ArgumentError] if +timeout+ is not positive
74
+ # @return [Integer] elapsed time sleeping
52
75
  def sleep(timeout = nil)
53
- if timeout != nil && !timeout.is_a?(Numeric)
54
- raise ArgumentError, "'timeout' argument must be nil or a Numeric"
55
- end
76
+ validate_timeout(timeout)
56
77
  unlock do
57
- if timeout
58
- elapsed_time = Kernel.sleep(timeout)
78
+ if timeout == nil || timeout == Float::INFINITY
79
+ elapsed_time = (timer { Thread.stop }).round
59
80
  else
60
- elapsed_time = Kernel.sleep
81
+ elapsed_time = Kernel.sleep(timeout)
61
82
  end
62
83
  end
63
84
  end
64
-
85
+
86
+ # Obtains the lock or blocks until the lock is available.
87
+ # @raise [ThreadError] if block not given
88
+ # @raise [ThreadError] if current thread is already locking it
89
+ # @raise [Exception] any exception raised in block
90
+ # @return [Object] value return from block
65
91
  def synchronize(&block)
92
+ raise ThreadError, 'must be called with a block' unless block_given?
66
93
  lock(&block)
67
94
  end
68
-
69
- # Attempts to obtain the lock and returns immediately.
95
+
96
+ # Attempts to obtain the lock and return immediately.
97
+ # @raise [ThreadError] if current thread is already locking it
70
98
  # @return [Boolean] returns if the lock was granted
71
99
  def try_lock
72
- raise ThreadError, 'this Thread is already locking this Mutex' if owned?
100
+ raise ThreadError, 'Attempt to lock a mutex which is already locked by this thread' if owned?
73
101
  @mutex.synchronize do
74
102
  if locked?
75
103
  false
@@ -79,25 +107,33 @@ module QuackConcurrency
79
107
  end
80
108
  end
81
109
  end
82
-
110
+
111
+ # @raise [ThreadError] if current thread is not locking it
112
+ #@overload unlock
113
+ # Releases the lock
114
+ # @return [void]
115
+ #@overload unlock(&block)
116
+ # Releases the lock, runs the block, then reacquires the lock when available,
117
+ # blocking if necessary.
118
+ # @raise [Exception] any exception raised in block
119
+ # @yield block to run while releasing the lock
120
+ # @return [Object] result of the block
83
121
  def unlock(&block)
84
122
  if block_given?
85
- unlock
86
- begin
87
- yield
88
- ensure
89
- lock
90
- end
123
+ temporarily_release(&block)
91
124
  else
92
125
  @mutex.synchronize do
93
- raise ThreadError, 'Mutex is not locked' unless locked?
94
- raise ThreadError, 'current Thread is not locking the Mutex' unless owned?
126
+ ensure_can_unlock
95
127
  if @condition_variable.any_waiting_threads?
96
128
  @condition_variable.signal
97
129
 
98
130
  # we do this to avoid a bug
99
- # consider what would happen if we set this to nil and then a thread called #lock
100
- # before the resuming thread was able to set itself at the owner in #lock
131
+ # consider this problem, imagine we have three threads:
132
+ # * A: this thread
133
+ # * B: has previously called #lock and is waiting on the @condition_variable
134
+ # * C: enters #lock after A has released the lock but before B has reacquired it
135
+ # is this scenario the threads may end up executing not in the chronological order
136
+ # that they entered #lock
101
137
  @owner = true
102
138
  else
103
139
  @owner = nil
@@ -106,16 +142,82 @@ module QuackConcurrency
106
142
  nil
107
143
  end
108
144
  end
109
-
145
+
146
+ # Returns the number of threads currently waiting on it.
147
+ # @return [Integer]
110
148
  def waiting_threads_count
111
149
  @condition_variable.waiting_threads_count
112
150
  end
113
-
151
+
114
152
  private
115
-
153
+
154
+ # Returns the current thread.
155
+ # @return [Thread]
116
156
  def caller
117
157
  Thread.current
118
158
  end
119
-
159
+
160
+ # Ensure it can be unlocked
161
+ # @raise [ThreadError] if it is not locked by the calling thread
162
+ def ensure_can_unlock
163
+ raise ThreadError, 'Attempt to unlock a ReentrantMutex which is not locked' unless locked?
164
+ raise ThreadError, 'Attempt to unlock a ReentrantMutex which is locked by another thread' unless owned?
165
+ end
166
+
167
+ # Try to immediately lock it.
168
+ # @api private
169
+ # @raise [ThreadError] if another thread is locking it
170
+ # @return [void]
171
+ def lock_immediately
172
+ unless try_lock
173
+ raise ThreadError, 'Attempt to lock a mutex which is locked by another thread'
174
+ end
175
+ end
176
+
177
+ # Temporarily unlocks it while a block is run.
178
+ # If an error is raised in the block the it will try to be immediately relocked
179
+ # before passing the error up. If unsuccessful, a +ThreadError+ will be raised to
180
+ # imitate the core's behavior.
181
+ # @api private
182
+ # @raise [ThreadError] if relock unsuccessful after an error
183
+ # @raise [ArgumentError] if no block given
184
+ # @return [void]
185
+ def temporarily_release(&block)
186
+ raise ArgumentError, 'no block given' unless block_given?
187
+ unlock
188
+ begin
189
+ return_value = yield
190
+ lock
191
+ rescue Exception
192
+ lock_immediately
193
+ raise
194
+ end
195
+ return_value
196
+ end
197
+
198
+ # Calculate time elapsed when running block.
199
+ # @api private
200
+ # @yield called while running timer
201
+ # @yieldparam start_time [Time]
202
+ # @raise [Exception] any exception raised in block
203
+ # @return [Float] time elapsed while running block
204
+ def timer(&block)
205
+ start_time = Time.now
206
+ yield(start_time)
207
+ time_elapsed = Time.now - start_time
208
+ end
209
+
210
+ # Validates a timeout value
211
+ # @api private
212
+ # @raise [TypeError] if {timeout} is not +nil+ or +Numeric+
213
+ # @raise [ArgumentError] if {timeout} is not positive
214
+ # @return [void]
215
+ def validate_timeout(timeout)
216
+ unless timeout == nil
217
+ raise TypeError, "'timeout' must be nil or a Numeric" unless timeout.is_a?(Numeric)
218
+ raise ArgumentError, "'timeout' must not be negative" if timeout.negative?
219
+ end
220
+ end
221
+
120
222
  end
121
223
  end
@@ -1,31 +1,34 @@
1
1
  module QuackConcurrency
2
-
3
- # @note duck type for +::Thread::Queue+
2
+
3
+ # This is a duck type for +::Thread::Queue+.
4
+ # It is intended to be a drop in replacement for it's core counterpart.
5
+ # Valuable if +::Thread::Queue+ has not been implemented.
4
6
  class Queue
5
-
7
+
6
8
  # Creates a new {Queue} concurrency tool.
7
9
  # @return [Queue]
8
10
  def initialize
11
+ @closed = false
12
+ @items = []
9
13
  @mutex = ::Mutex.new
10
14
  @pop_mutex = Mutex.new
11
15
  @waiter = Waiter.new
12
- @items = []
13
- @closed = false
14
16
  end
15
-
16
- # Removes all objects from the {Queue}.
17
+
18
+ # Removes all objects from it.
17
19
  # @return [self]
18
20
  def clear
19
21
  @mutex.synchronize { @items.clear }
20
22
  self
21
23
  end
22
-
23
- # Closes the {Queue}. A closed {Queue} cannot be re-opened.
24
+
25
+ # Closes it.
26
+ # Once closed, it cannot be re-opened.
24
27
  # After the call to close completes, the following are true:
25
- # * {#closed?} will return `true`.
28
+ # * {#closed?} will return +true+.
26
29
  # * {#close} will be ignored.
27
30
  # * {#push} will raise an exception.
28
- # * until empty, calling {#pop} will return an object from the {Queue} as usual.
31
+ # * until empty, calling {#pop} will return an object from it as usual.
29
32
  # @return [self]
30
33
  def close
31
34
  @mutex.synchronize do
@@ -35,37 +38,38 @@ module QuackConcurrency
35
38
  end
36
39
  self
37
40
  end
38
-
39
- # Checks if {Queue} is closed.
41
+
42
+ # Checks if it is closed.
40
43
  # @return [Boolean]
41
44
  def closed?
42
45
  @closed
43
46
  end
44
-
45
- # Checks if {Queue} is empty.
47
+
48
+ # Checks if it is empty.
46
49
  # @return [Boolean]
47
50
  def empty?
48
51
  @items.empty?
49
52
  end
50
-
51
- # Returns the length of the {Queue}.
53
+
54
+ # Returns the length of it.
52
55
  # @return [Integer]
53
56
  def length
54
57
  @items.length
55
58
  end
56
59
  alias_method :size, :length
57
-
58
- # Returns the number of threads waiting on the {Queue}.
60
+
61
+ # Returns the number of threads waiting on it.
59
62
  # @return [Integer]
60
63
  def num_waiting
61
64
  @pop_mutex.waiting_threads_count + @waiter.waiting_threads_count
62
65
  end
63
-
64
- # Retrieves item from the {Queue}.
65
- # @note If the {Queue} is empty, it will block until an item is available.
66
- # If `non_block` is `true`, it will raise {ThreadError} instead.
67
- # @raise {ThreadError} if {Queue} is empty and `non_block` is `true`
66
+
67
+ # Retrieves an item from it.
68
+ # @note If it is empty, the method will block until an item is available.
69
+ # If +non_block+ is +true+, a +ThreadError+ will be raised.
70
+ # @raise [ThreadError] if it is empty and +non_block+ is +true+
68
71
  # @param non_block [Boolean]
72
+ # @return [Object]
69
73
  def pop(non_block = false)
70
74
  @pop_mutex.lock do
71
75
  @mutex.synchronize do
@@ -83,20 +87,20 @@ module QuackConcurrency
83
87
  end
84
88
  alias_method :deq, :pop
85
89
  alias_method :shift, :pop
86
-
87
- # Pushes the given object to the {Queue}.
90
+
91
+ # Pushes the given object to it.
88
92
  # @param item [Object]
89
93
  # @return [self]
90
94
  def push(item = nil)
91
95
  @mutex.synchronize do
92
96
  raise ClosedQueueError if closed?
93
97
  @items.push(item)
94
- @waiter.resume_one
98
+ @waiter.resume_next
95
99
  end
96
100
  self
97
101
  end
98
102
  alias_method :<<, :push
99
103
  alias_method :enq, :push
100
-
104
+
101
105
  end
102
106
  end
@@ -2,32 +2,41 @@
2
2
 
3
3
 
4
4
  module QuackConcurrency
5
+
6
+ # {ReentrantMutex}s are similar to {Mutex}s with with the key distinction being
7
+ # that a thread can call lock on a {Mutex} that it has already locked.
5
8
  class ReentrantMutex < Mutex
6
-
9
+
7
10
  # Creates a new {ReentrantMutex} concurrency tool.
8
11
  # @return [ReentrantMutex]
9
12
  def initialize
10
13
  super
11
14
  @lock_depth = 0
12
15
  end
13
-
16
+
14
17
  #@overload lock
15
- # Obtains the lock or sleeps the current `Thread` until it is available.
18
+ # Obtains a lock, blocking until available.
19
+ # It will acquire a lock even if one is already held.
16
20
  # @return [void]
17
21
  #@overload lock(&block)
18
- # Obtains the lock, runs the block, then releases the lock when the block completes.
22
+ # Obtains a lock, runs the block, then releases a lock.
23
+ # It will block until a lock is available.
24
+ # It will acquire a lock even if one is already held.
25
+ # @raise [ThreadError] if not locked by the calling thread when unlocking
26
+ # @raise [ThreadError] if not holding the same lock count when unlocking
27
+ # @raise [Exception] any exception raised in block
19
28
  # @yield block to run with the lock
20
29
  # @return [Object] result of the block
21
30
  def lock(&block)
22
31
  if block_given?
23
32
  lock
24
33
  start_depth = @lock_depth
25
- start_owner = owner
26
34
  begin
27
35
  yield
28
36
  ensure
29
- unless @lock_depth == start_depth && owner == start_owner
30
- raise Error, 'could not unlock reentrant mutex as its state has been modified'
37
+ ensure_can_unlock
38
+ unless @lock_depth == start_depth
39
+ raise ThreadError, 'Attempt to unlock a ReentrantMutex whose lock depth has been changed since locking it'
31
40
  end
32
41
  unlock
33
42
  end
@@ -37,105 +46,84 @@ module QuackConcurrency
37
46
  nil
38
47
  end
39
48
  end
40
-
41
- # Checks if this {ReentrantMutex} is locked by a Thread other than the caller.
42
- # @return [Boolean]
43
- def locked_out?
44
- # don't need a mutex because we know #owned? can't change during the call
45
- locked? && !owned?
46
- end
47
-
48
- # Releases the lock and sleeps.
49
- # When the calling Thread is next woken up, it will attempt to reacquire the lock.
50
- # @param timeout [Integer] seconds to sleep, `nil` will sleep forever
51
- # @raise [Error] if this {ReentrantMutex} wasn't locked by the calling Thread
52
- # @return [void]
49
+
50
+ # @see Mutex#sleep
53
51
  def sleep(timeout = nil)
54
- raise Error, 'can not unlock reentrant mutex, it is not locked' unless locked?
55
- raise Error, 'can not unlock reentrant mutex, caller is not the owner' unless owned?
52
+ ensure_can_unlock
56
53
  base_depth do
57
54
  super(timeout)
58
55
  end
59
56
  end
60
-
61
- # Obtains a lock, runs the block, and releases the lock when the block completes.
62
- # @return [Object] value from yielded block
63
- def synchronize(&block)
64
- lock(&block)
65
- end
66
-
67
- alias parent_try_lock try_lock
68
- private :parent_try_lock
57
+
69
58
  # Attempts to obtain the lock and returns immediately.
70
59
  # @return [Boolean] returns if the lock was granted
71
60
  def try_lock
72
- if owned?
61
+ if owned? || super
73
62
  @lock_depth += 1
74
63
  true
75
64
  else
76
- lock_successful = parent_try_lock
77
- if lock_successful
78
- @lock_depth += 1
79
- true
80
- else
81
- false
82
- end
65
+ false
83
66
  end
84
67
  end
85
-
86
- # Releases the lock.
87
- # @raise [Error] if {ReentrantMutex} wasn't locked by the calling Thread
88
- # @return [void]
68
+
69
+ #@overload unlock
70
+ # Releases a lock.
71
+ # @return [void]
72
+ #@overload unlock(&block)
73
+ # Releases a lock, runs the block, then reacquires the lock when available,
74
+ # blocking if necessary.
75
+ # @raise [Exception] any exception raised in block
76
+ # @raise [ThreadError] if relock unsuccessful after an error
77
+ # @yield block to run while releasing the lock
78
+ # @return [Object] result of the block
79
+ # @raise [ThreadError] if it is not locked by this thread
89
80
  def unlock(&block)
90
- raise Error, 'can not unlock reentrant mutex, it is not locked' unless locked?
91
- raise Error, 'can not unlock reentrant mutex, caller is not the owner' unless owned?
81
+ ensure_can_unlock
92
82
  if block_given?
93
- unlock
94
- begin
95
- yield
96
- ensure
97
- lock
98
- end
83
+ temporarily_release(&block)
99
84
  else
100
85
  @lock_depth -= 1
101
86
  super if @lock_depth == 0
102
87
  nil
103
88
  end
104
89
  end
105
-
106
- # Releases the lock.
107
- # @raise [Error] if {ReentrantMutex} wasn't locked by the calling Thread
108
- # @return [void]
90
+
91
+ # Releases all lock, runs the block, then reacquires the same lock count when available,
92
+ # blocking if necessary.
93
+ # @raise [ArgumentError] if no block given
94
+ # @raise [ThreadError] if this thread does not hold any locks
95
+ # @raise [Exception] any exception raised in block
96
+ # @yield block to run while locks have been released
97
+ # @return [Object] result of the block
109
98
  def unlock!(&block)
110
- raise Error, 'can not unlock reentrant mutex, it is not locked' unless locked?
111
- raise Error, 'can not unlock reentrant mutex, caller is not the owner' unless owned?
112
- if block_given?
113
- base_depth do
114
- unlock
115
- begin
116
- yield
117
- ensure
118
- lock
119
- end
120
- end
121
- else
122
- @lock_depth = 0
123
- super
124
- nil
99
+ ensure_can_unlock
100
+ base_depth do
101
+ temporarily_release(&block)
125
102
  end
126
103
  end
127
-
104
+
128
105
  private
129
-
106
+
107
+ # Releases all but one lock, runs the block, then reacquires the released lock count when available,
108
+ # blocking if necessary.
130
109
  # @api private
110
+ # @raise [Exception] any exception raised in block
111
+ # @return [Object] result of the block
131
112
  def base_depth(&block)
132
113
  start_depth = @lock_depth
133
114
  @lock_depth = 1
134
- yield
135
- ensure
115
+ return_value = yield
136
116
  @lock_depth = start_depth
117
+ return_value
137
118
  end
138
-
119
+
120
+ # Ensure it can be unlocked
121
+ # @raise [ThreadError] if it is not locked by this thread
122
+ # @return [void]
123
+ def ensure_can_unlock
124
+ raise ThreadError, 'Attempt to unlock a ReentrantMutex which is not locked' unless locked?
125
+ raise ThreadError, 'Attempt to unlock a ReentrantMutex which is locked by another thread' unless owned?
126
+ end
127
+
139
128
  end
140
129
  end
141
-