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