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
@@ -0,0 +1,23 @@
1
+ module QuackConcurrency
2
+
3
+ # {SafeConditionVariable} is similar to {ConditionVariable}.
4
+ #
5
+ # The key distinction is that every call to {#wait} can only be resumed via
6
+ # the {SafeConditionVariable} (not with +Thread#run+, +Thread#wakeup+, etc.)
7
+ class SafeConditionVariable < ConditionVariable
8
+
9
+ # #@!method wait
10
+ # Puts this thread to sleep until another thread resumes it via this {SafeConditionVariable}.
11
+ # @see ConditionVariable#wait
12
+
13
+ private
14
+
15
+ # Returns a waitable object for current thread.
16
+ # @api private
17
+ # @return [Waitable]
18
+ def waitable_for_current_thread
19
+ Waitable.new(self)
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ module QuackConcurrency
2
+ class SafeConditionVariable
3
+
4
+ # @see ConditionVariable::Waitable
5
+ # Uses {SafeSleeper}s to ensure the thread can only be woken by this {SafeConditionVariable}.
6
+ class Waitable < ConditionVariable::Waitable
7
+
8
+ # Creates a new {Waitable}.
9
+ # @return [Waitable]
10
+ def initialize(condition_variable)
11
+ super(condition_variable)
12
+ @sleeper = SafeSleeper.new
13
+ end
14
+
15
+ # @!method wait
16
+ # Can only be resumed via {#resume}.
17
+ # @see ConditionVariable#wait
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,80 @@
1
+ module QuackConcurrency
2
+
3
+ # A {SafeSleeper} can be used to safely sleep a thread or preemptively wake it.
4
+ #
5
+ # Unlike simply calling +Thread#sleep+, {#sleep} will ensure that only
6
+ # calling {#wake} on this {SafeSleeper} will wake the thread.
7
+ # Any call to +Thread#run+ directly, will be ignored.
8
+ # Threads are still be resumed if +Thread#raise+ is called which may cause
9
+ # problems so it should never be used.
10
+ # A thread can only be put to sleep and woken once for each {SafeSleeper}.
11
+ class SafeSleeper < Sleeper
12
+
13
+ # Creates a new {SafeSleeper} concurrency tool.
14
+ # @return [SafeSleeper]
15
+ def initialize
16
+ super
17
+ @state = :initial
18
+ end
19
+
20
+ # @see SafeSleeper#sleep
21
+ def sleep(timeout = nil)
22
+ timer do |start_time|
23
+ deadline = wake_deadline(start_time, timeout)
24
+ enforce_sleep_call_limit
25
+ @mutex.synchronize do
26
+ break if @state == :complete
27
+ @state == :sleep
28
+ wait(deadline)
29
+ ensure
30
+ @state = :complete
31
+ end
32
+ end
33
+ end
34
+
35
+ # @see SafeSleeper#wake
36
+ def wake
37
+ @mutex.synchronize do
38
+ enforce_wake_call_limit
39
+ @state = :complete
40
+ @condition_variable.signal
41
+ end
42
+ nil
43
+ end
44
+
45
+ private
46
+
47
+ # Put this thread to sleep and wait for it to be woken.
48
+ # Will wake if {#wake} is called.
49
+ # If called with a +deadline+ it will wake when +deadline+ is reached.
50
+ # @api private
51
+ # @param deadline [nil,Time] maximum time to sleep, +nil+ for forever
52
+ # @raise [Exception] any exception raised by +ConditionVariable#wait+ (eg. interrupts, +ThreadError+)
53
+ # @return [void]
54
+ def wait(deadline)
55
+ loop do
56
+ if deadline
57
+ remaining = deadline - Time.now
58
+ @condition_variable.wait(@mutex, remaining) if remaining > 0
59
+ else
60
+ @condition_variable.wait(@mutex)
61
+ end
62
+ break if @state == :complete
63
+ break if deadline && Time.now >= deadline
64
+ end
65
+ end
66
+
67
+ # Calculate the desired time to wake up.
68
+ # @api private
69
+ # @param start_time [nil,Time] time when the thread is put to sleep
70
+ # @param timeout [Numeric] desired time to sleep in seconds, +nil+ for forever
71
+ # @raise [TypeError] if +start_time+ is not +nil+ or a +Numeric+
72
+ # @raise [ArgumentError] if +start_time+ is negative
73
+ # @return [Time]
74
+ def wake_deadline(start_time, timeout)
75
+ timeout = process_timeout(timeout)
76
+ deadline = start_time + timeout if timeout
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,100 @@
1
+ module QuackConcurrency
2
+
3
+ # A {Sleeper} can be used to preemptively wake a thread that will be put to sleep in the future.
4
+ #
5
+ # A thread can only be put to sleep and woken once for each {Sleeper}.
6
+ class Sleeper
7
+
8
+ # Creates a new {Sleeper} concurrency tool.
9
+ # @return [Sleeper]
10
+ def initialize
11
+ @state = :initial
12
+ @mutex = ::Mutex.new
13
+ @condition_variable = ::ConditionVariable.new
14
+ @sleep_called = false
15
+ @wake_called = false
16
+ end
17
+
18
+ # Puts this thread to sleep.
19
+ # Will be skipped if {#wake} has already been called.
20
+ # If called without a timeout it will sleep forever.
21
+ # It can only be called once.
22
+ # @param timeout [nil,Numeric] maximum time to sleep in seconds, +nil+ for forever
23
+ # @raise [TypeError] if +timeout+ is not +nil+ or +Numeric+
24
+ # @raise [ArgumentError] if +timeout+ is negative
25
+ # @raise [RuntimeError] if already called once
26
+ # @raise [Exception] any exception raised by +ConditionVariable#wait+ (eg. interrupts, +ThreadError+)
27
+ # @return [Float] duration of time the thread was asleep in seconds
28
+ def sleep(timeout = nil)
29
+ timeout = process_timeout(timeout)
30
+ enforce_sleep_call_limit
31
+ @mutex.synchronize do
32
+ timer do
33
+ @condition_variable.wait(@mutex, timeout) unless @wake_called
34
+ end
35
+ end
36
+ end
37
+
38
+ # Wake it's sleeping thread, if one exists.
39
+ # It can only be called once.
40
+ # @raise [RuntimeError] if already called once
41
+ # @return [void]
42
+ def wake
43
+ @mutex.synchronize do
44
+ enforce_wake_call_limit
45
+ @condition_variable.signal
46
+ end
47
+ nil
48
+ end
49
+
50
+ private
51
+
52
+ # Ensure {#sleep} is not called more than once.
53
+ # Call this every time {#sleep} is called.
54
+ # @api private
55
+ # @raise [RuntimeError] if called more than once
56
+ # @return [void]
57
+ def enforce_sleep_call_limit
58
+ raise RuntimeError, '#sleep has already been called once' if @sleep_called
59
+ @sleep_called = true
60
+ end
61
+
62
+ # Ensure {#wake} is not called more than once.
63
+ # Call this every time {#wake} is called.
64
+ # @api private
65
+ # @raise [RuntimeError] if called more than once
66
+ # @return [void]
67
+ def enforce_wake_call_limit
68
+ raise RuntimeError, '#wake has already been called once' if @wake_called
69
+ @wake_called = true
70
+ end
71
+
72
+ # Calculate time elapsed when running a block.
73
+ # @api private
74
+ # @yield called while running timer
75
+ # @yieldparam start_time [Time]
76
+ # @raise [Exception] any exception raised in block
77
+ # @return [Float] time elapsed while running block
78
+ def timer(&block)
79
+ start_time = Time.now
80
+ yield(start_time)
81
+ time_elapsed = Time.now - start_time
82
+ end
83
+
84
+ # Validates a timeout value, converting to a acceptable value if necessary
85
+ # @api private
86
+ # @param timeout [nil,Numeric]
87
+ # @raise [TypeError] if +timeout+ is not +nil+ or +Numeric+
88
+ # @raise [ArgumentError] if +timeout+ is negative
89
+ # @return [nil,Numeric]
90
+ def process_timeout(timeout)
91
+ unless timeout == nil
92
+ raise TypeError, "'timeout' must be nil or a Numeric" unless timeout.is_a?(Numeric)
93
+ raise ArgumentError, "'timeout' must not be negative" if timeout.negative?
94
+ end
95
+ timeout = nil if timeout == Float::INFINITY
96
+ timeout
97
+ end
98
+
99
+ end
100
+ end
@@ -1,58 +1,67 @@
1
1
  module QuackConcurrency
2
-
2
+
3
+ # {Waiter} is similar to {ConditionVariable}.
4
+ #
5
+ # A few differences include:
6
+ # * the ability to force any future request to {#wait} to return immediately
7
+ # * every call to {#wait} can only be resumed via the {Waiter}
8
+ # (not with +Thread#run+, +Thread#wakeup+, etc.)
9
+ # * {#wait} does not accept a mutex
10
+ # * some methods have been renamed to be more intuitive
3
11
  # @api private
4
12
  class Waiter
5
-
13
+
6
14
  # Creates a new {Waiter} concurrency tool.
7
15
  # @return [Waiter]
8
16
  def initialize
9
- @condition_variable = UninterruptibleConditionVariable.new
10
- @resume_all_forever = false
17
+ @condition_variable = SafeConditionVariable.new
18
+ @resume_all_indefinitely = false
11
19
  @mutex = ::Mutex.new
12
20
  end
13
-
21
+
22
+ # Checks if any threads are waiting on it.
23
+ # @return [Boolean]
14
24
  def any_waiting_threads?
15
25
  @condition_variable.any_waiting_threads?
16
26
  end
17
-
18
- # Resumes all current and future waiting Thread.
27
+
28
+ # Resumes all threads waiting on it.
19
29
  # @return [void]
20
30
  def resume_all
21
31
  @condition_variable.broadcast
22
- nil
23
32
  end
24
-
25
- # Resumes all current and future waiting Thread.
33
+
34
+ # Resumes all threads waiting on it and will cause
35
+ # any future calls to {#wait} to return immediately.
26
36
  # @return [void]
27
- def resume_all_forever
37
+ def resume_all_indefinitely
28
38
  @mutex.synchronize do
29
- @resume_all_forever = true
39
+ @resume_all_indefinitely = true
30
40
  resume_all
31
41
  end
32
- nil
33
42
  end
34
-
35
- # Resumes next waiting Thread if one exists.
43
+
44
+ # Resumes next thread waiting on it if one exists.
36
45
  # @return [void]
37
- def resume_one
46
+ def resume_next
38
47
  @condition_variable.signal
39
- nil
40
48
  end
41
-
42
- # Waits for another Thread to resume the calling Thread.
49
+
50
+ # Puts this thread to sleep until another thread resumes it via this {Waiter}.
43
51
  # @note Will block until resumed.
44
52
  # @return [void]
45
53
  def wait
46
54
  @mutex.synchronize do
47
- return if @resume_all_forever
55
+ return if @resume_all_indefinitely
48
56
  @condition_variable.wait(@mutex)
49
57
  end
50
- nil
51
58
  end
52
-
59
+
60
+ # Returns the number of threads waiting on it.
61
+ # @return [Integer]
53
62
  def waiting_threads_count
54
63
  @condition_variable.waiting_threads_count
55
64
  end
56
-
65
+
57
66
  end
58
67
  end
@@ -0,0 +1,216 @@
1
+ require 'quack_concurrency'
2
+
3
+ describe QuackConcurrency::ConditionVariable do
4
+
5
+ describe "::new" do
6
+
7
+ context "when called with no arguments" do
8
+ it "should return a ConditionVariable" do
9
+ condition_variable = QuackConcurrency::ConditionVariable.new
10
+ expect(condition_variable).to be_a(QuackConcurrency::ConditionVariable)
11
+ end
12
+ end
13
+
14
+ end
15
+
16
+ describe "#any_waiting_threads?" do
17
+
18
+ context "when called with waiting threads" do
19
+ it "should return true" do
20
+ condition_variable = QuackConcurrency::ConditionVariable.new
21
+ mutex = Mutex.new
22
+ thread = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
23
+ sleep 1
24
+ expect(condition_variable.any_waiting_threads?).to be true
25
+ condition_variable.broadcast
26
+ thread.join
27
+ end
28
+ end
29
+
30
+ context "when called with no waiting threads" do
31
+ it "should return false" do
32
+ condition_variable = QuackConcurrency::ConditionVariable.new
33
+ expect(condition_variable.any_waiting_threads?).to be false
34
+ end
35
+ end
36
+
37
+ end
38
+
39
+ describe "#broadcast" do
40
+
41
+ context "when called with waiting threads" do
42
+ it "should resume all threads currently waiting" do
43
+ condition_variable = QuackConcurrency::ConditionVariable.new
44
+ mutex = ::Mutex.new
45
+ thread1 = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
46
+ thread2 = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
47
+ sleep 1
48
+ condition_variable.broadcast
49
+ sleep 1
50
+ expect(thread1.alive?).to be false
51
+ expect(thread2.alive?).to be false
52
+ end
53
+ end
54
+
55
+ context "when called with no waiting threads" do
56
+ it "should not raise an error" do
57
+ condition_variable = QuackConcurrency::ConditionVariable.new
58
+ expect{ condition_variable.broadcast }.not_to raise_error
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ describe "#signal" do
65
+
66
+ context "when called with waiting threads" do
67
+ it "should resume the next thread currently waiting" do
68
+ condition_variable = QuackConcurrency::ConditionVariable.new
69
+ mutex = Mutex.new
70
+ values = []
71
+ Thread.new { mutex.synchronize { condition_variable.wait(mutex); values << 1 } }
72
+ Thread.new { sleep 1; mutex.synchronize { condition_variable.wait(mutex); values << 2 } }
73
+ Thread.new { sleep 2; mutex.synchronize { condition_variable.wait(mutex); values << 3 } }
74
+ sleep 3
75
+ condition_variable.signal
76
+ condition_variable.signal
77
+ condition_variable.signal
78
+ sleep 1
79
+ expect(values).to eq [1, 2, 3]
80
+ end
81
+ end
82
+
83
+ context "when called with no waiting threads" do
84
+ it "should not raise an error" do
85
+ condition_variable = QuackConcurrency::ConditionVariable.new
86
+ expect{ condition_variable.signal }.not_to raise_error
87
+ end
88
+ end
89
+
90
+ end
91
+
92
+ describe "#wait" do
93
+
94
+ context "when called without a timeout" do
95
+ it "should block until #broadcast or #signal are called" do
96
+ condition_variable = QuackConcurrency::ConditionVariable.new
97
+ mutex = Mutex.new
98
+ thread1 = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
99
+ thread2 = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
100
+ sleep 1
101
+ expect(thread1.alive?).to be true
102
+ expect(thread2.alive?).to be true
103
+ condition_variable.broadcast
104
+ sleep 1
105
+ expect(thread1.alive?).to be false
106
+ expect(thread2.alive?).to be false
107
+ end
108
+ context "and before Thread#run" do
109
+ it "should return after Thread#run is called" do
110
+ condition_variable = QuackConcurrency::ConditionVariable.new
111
+ mutex = Mutex.new
112
+ thread = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
113
+ sleep 1
114
+ thread.run
115
+ sleep 1
116
+ expect(thread.alive?).to be false
117
+ end
118
+ end
119
+ end
120
+
121
+ context "when called with a timeout" do
122
+ context "of nil" do
123
+ it "should return only after #broadcast or #signal are called" do
124
+ condition_variable = QuackConcurrency::ConditionVariable.new
125
+ mutex = Mutex.new
126
+ thread = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
127
+ sleep 1
128
+ expect(thread.alive?).to be true
129
+ condition_variable.broadcast
130
+ sleep 1
131
+ expect(thread.alive?).to be false
132
+ end
133
+ end
134
+ context "of Float::INFINITY" do
135
+ it "should return only after #broadcast or #signal are called" do
136
+ condition_variable = QuackConcurrency::ConditionVariable.new
137
+ mutex = Mutex.new
138
+ thread = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
139
+ sleep 1
140
+ expect(thread.alive?).to be true
141
+ condition_variable.broadcast
142
+ sleep 1
143
+ expect(thread.alive?).to be false
144
+ end
145
+ end
146
+ context "of non Numeric value" do
147
+ it "should raise TypeError" do
148
+ condition_variable = QuackConcurrency::ConditionVariable.new
149
+ mutex = Mutex.new
150
+ expect{ mutex.synchronize { condition_variable.wait(mutex, '1') } }.to raise_error(TypeError)
151
+ end
152
+ end
153
+ context "of negative Numeric value" do
154
+ it "should raise ArgumentError" do
155
+ condition_variable = QuackConcurrency::ConditionVariable.new
156
+ mutex = Mutex.new
157
+ expect{ mutex.synchronize { condition_variable.wait(mutex, -1) } }.to raise_error(ArgumentError)
158
+ end
159
+ end
160
+ context "of positive Integer" do
161
+ it "should block until timeout reached" do
162
+ condition_variable = QuackConcurrency::ConditionVariable.new
163
+ mutex = Mutex.new
164
+ thread = Thread.new { mutex.synchronize { condition_variable.wait(mutex, 2) } }
165
+ sleep 1
166
+ expect(thread.alive?).to be true
167
+ sleep 2
168
+ expect(thread.alive?).to be false
169
+ end
170
+ end
171
+ context "and Thread#run is called before timeout is reached" do
172
+ it "should return after Thread#run is called" do
173
+ condition_variable = QuackConcurrency::ConditionVariable.new
174
+ mutex = Mutex.new
175
+ thread = Thread.new { mutex.synchronize { condition_variable.wait(mutex, 3) } }
176
+ sleep 1
177
+ thread.run
178
+ sleep 1
179
+ expect(thread.alive?).to be false
180
+ end
181
+ end
182
+ end
183
+
184
+ end
185
+
186
+ describe "#waiting_threads_count" do
187
+
188
+ context "when called" do
189
+ it "should return a Integer" do
190
+ condition_variable = QuackConcurrency::ConditionVariable.new
191
+ expect(condition_variable.waiting_threads_count).to be_a(Integer)
192
+ end
193
+ end
194
+
195
+ context "when called with no waiting threads" do
196
+ it "should return 0" do
197
+ condition_variable = QuackConcurrency::ConditionVariable.new
198
+ expect(condition_variable.waiting_threads_count).to eq(0)
199
+ end
200
+ end
201
+
202
+ context "when called with one waiting thread" do
203
+ it "should return 1" do
204
+ condition_variable = QuackConcurrency::ConditionVariable.new
205
+ mutex = Mutex.new
206
+ thread = Thread.new { mutex.synchronize { condition_variable.wait(mutex) } }
207
+ sleep 1
208
+ expect(condition_variable.waiting_threads_count).to eq(1)
209
+ condition_variable.broadcast
210
+ thread.join
211
+ end
212
+ end
213
+
214
+ end
215
+
216
+ end