quack_concurrency 0.5.4 → 0.6.0

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