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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/quack_concurrency.rb +6 -13
- data/lib/quack_concurrency/condition_variable.rb +91 -85
- data/lib/quack_concurrency/condition_variable/waitable.rb +108 -0
- data/lib/quack_concurrency/error.rb +1 -1
- data/lib/quack_concurrency/future.rb +31 -30
- data/lib/quack_concurrency/future/canceled.rb +1 -0
- data/lib/quack_concurrency/future/complete.rb +1 -0
- data/lib/quack_concurrency/mutex.rb +140 -38
- data/lib/quack_concurrency/queue.rb +32 -28
- data/lib/quack_concurrency/reentrant_mutex.rb +64 -76
- data/lib/quack_concurrency/safe_condition_variable.rb +23 -0
- data/lib/quack_concurrency/safe_condition_variable/waitable.rb +21 -0
- data/lib/quack_concurrency/safe_sleeper.rb +80 -0
- data/lib/quack_concurrency/sleeper.rb +100 -0
- data/lib/quack_concurrency/waiter.rb +32 -23
- data/spec/condition_variable_spec.rb +216 -0
- data/spec/future_spec.rb +145 -79
- data/spec/mutex_spec.rb +441 -0
- data/spec/queue_spec.rb +217 -77
- data/spec/reentrant_mutex_spec.rb +394 -99
- data/spec/safe_condition_variable_spec.rb +115 -0
- data/spec/safe_sleeper_spec.rb +197 -0
- data/spec/sleeper.rb +197 -0
- data/spec/waiter_spec.rb +181 -0
- metadata +16 -14
- data/lib/quack_concurrency/queue/error.rb +0 -6
- data/lib/quack_concurrency/reentrant_mutex/error.rb +0 -6
- data/lib/quack_concurrency/semaphore.rb +0 -139
- data/lib/quack_concurrency/semaphore/error.rb +0 -6
- data/lib/quack_concurrency/uninterruptible_condition_variable.rb +0 -94
- data/lib/quack_concurrency/uninterruptible_sleeper.rb +0 -81
- data/lib/quack_concurrency/yielder.rb +0 -35
- data/spec/semaphore_spec.rb +0 -244
data/spec/waiter_spec.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
require 'quack_concurrency'
|
2
|
+
|
3
|
+
describe QuackConcurrency::Waiter do
|
4
|
+
|
5
|
+
# arbitrary amount of time we will wait for everything to settle
|
6
|
+
def delay(units = 1)
|
7
|
+
sleep units
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "::new" do
|
11
|
+
|
12
|
+
context "when called with no arguments" do
|
13
|
+
waiter = nil
|
14
|
+
it "should not raise error" do
|
15
|
+
expect{ waiter = QuackConcurrency::Waiter.new }.not_to raise_error
|
16
|
+
end
|
17
|
+
it "should return a Waiter" do
|
18
|
+
expect(waiter).to be_a(QuackConcurrency::Waiter)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#any_waiting_threads?" do
|
25
|
+
|
26
|
+
context "when called with waiting threads" do
|
27
|
+
it "should return true" do
|
28
|
+
waiter = QuackConcurrency::Waiter.new
|
29
|
+
thread = Thread.new { waiter.wait }
|
30
|
+
delay(1)
|
31
|
+
expect(waiter.any_waiting_threads?).to be true
|
32
|
+
waiter.resume_all
|
33
|
+
thread.join
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "when called with no waiting threads" do
|
38
|
+
it "should return false" do
|
39
|
+
waiter = QuackConcurrency::Waiter.new
|
40
|
+
expect(waiter.any_waiting_threads?).to be false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#resume_all" do
|
47
|
+
|
48
|
+
context "when called" do
|
49
|
+
it "should resume all threads currently waiting" do
|
50
|
+
waiter = QuackConcurrency::Waiter.new
|
51
|
+
thread1 = Thread.new { waiter.wait }
|
52
|
+
thread2 = Thread.new { waiter.wait }
|
53
|
+
delay(1)
|
54
|
+
waiter.resume_all
|
55
|
+
delay(1)
|
56
|
+
expect(thread1.alive?).to be false
|
57
|
+
expect(thread2.alive?).to be false
|
58
|
+
end
|
59
|
+
it "should not resume any future threads that call #wait" do
|
60
|
+
waiter = QuackConcurrency::Waiter.new
|
61
|
+
waiter.resume_all
|
62
|
+
thread = Thread.new { waiter.wait }
|
63
|
+
delay(1)
|
64
|
+
expect(thread.alive?).to be true
|
65
|
+
waiter.resume_all
|
66
|
+
thread.join
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#resume_all_indefinitely" do
|
73
|
+
|
74
|
+
context "when called" do
|
75
|
+
it "should resume all threads currently waiting" do
|
76
|
+
waiter = QuackConcurrency::Waiter.new
|
77
|
+
thread1 = Thread.new { waiter.wait }
|
78
|
+
thread2 = Thread.new { waiter.wait }
|
79
|
+
delay(1)
|
80
|
+
waiter.resume_all_indefinitely
|
81
|
+
delay(1)
|
82
|
+
expect(thread1.alive?).to be false
|
83
|
+
expect(thread2.alive?).to be false
|
84
|
+
end
|
85
|
+
it "should resume all future threads that call #wait" do
|
86
|
+
waiter = QuackConcurrency::Waiter.new
|
87
|
+
waiter.resume_all_indefinitely
|
88
|
+
thread1 = Thread.new { waiter.wait }
|
89
|
+
thread2 = Thread.new { waiter.wait }
|
90
|
+
delay(1)
|
91
|
+
expect(thread1.alive?).to be false
|
92
|
+
expect(thread2.alive?).to be false
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
describe "#resume_next" do
|
99
|
+
|
100
|
+
context "when called" do
|
101
|
+
it "should resume the next thread currently waiting" do
|
102
|
+
waiter = QuackConcurrency::Waiter.new
|
103
|
+
thread1 = Thread.new { waiter.wait }
|
104
|
+
delay(1)
|
105
|
+
thread2 = Thread.new { waiter.wait }
|
106
|
+
delay(1)
|
107
|
+
waiter.resume_next
|
108
|
+
delay(1)
|
109
|
+
expect(thread1.alive?).to be false
|
110
|
+
expect(thread2.alive?).to be true
|
111
|
+
waiter.resume_next
|
112
|
+
delay(1)
|
113
|
+
expect(thread2.alive?).to be false
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
describe "#wait" do
|
120
|
+
|
121
|
+
context "when called" do
|
122
|
+
it "should return only after one of the resume methods are called" do
|
123
|
+
waiter = QuackConcurrency::Waiter.new
|
124
|
+
thread1 = Thread.new { waiter.wait }
|
125
|
+
thread2 = Thread.new { waiter.wait }
|
126
|
+
delay(1)
|
127
|
+
expect(thread1.alive?).to be true
|
128
|
+
expect(thread2.alive?).to be true
|
129
|
+
waiter.resume_all
|
130
|
+
thread1.join
|
131
|
+
thread2.join
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context "when called before Thread#run" do
|
136
|
+
it "should return only after one of the resume methods are called" do
|
137
|
+
waiter = QuackConcurrency::Waiter.new
|
138
|
+
elapsed_time = nil
|
139
|
+
thread = Thread.new { waiter.wait }
|
140
|
+
delay(1)
|
141
|
+
thread.run
|
142
|
+
delay(1)
|
143
|
+
expect(thread.alive?).to be true
|
144
|
+
waiter.resume_all
|
145
|
+
delay(1)
|
146
|
+
expect(thread.alive?).to be false
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "#waiting_threads_count" do
|
153
|
+
|
154
|
+
context "when called" do
|
155
|
+
it "should return a Integer" do
|
156
|
+
waiter = QuackConcurrency::Waiter.new
|
157
|
+
expect(waiter.waiting_threads_count).to be_a(Integer)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context "when called with no waiting threads" do
|
162
|
+
it "should return 0" do
|
163
|
+
waiter = QuackConcurrency::Waiter.new
|
164
|
+
expect(waiter.waiting_threads_count).to be 0
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context "when called with one waiting thread" do
|
169
|
+
it "should return 1" do
|
170
|
+
waiter = QuackConcurrency::Waiter.new
|
171
|
+
thread = Thread.new { waiter.wait }
|
172
|
+
delay(1)
|
173
|
+
expect(waiter.waiting_threads_count).to be 1
|
174
|
+
waiter.resume_next
|
175
|
+
thread.join
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: quack_concurrency
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob Fors
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-12-
|
11
|
+
date: 2018-12-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: reentrant_mutex
|
@@ -25,8 +25,7 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.1'
|
27
27
|
description: Offers concurrency tools that could also be found in the 'Concurrent
|
28
|
-
Ruby'. However, all these tools will also accept core
|
29
|
-
off of.
|
28
|
+
Ruby'. However, all these tools will also accept core duck types to build off of.
|
30
29
|
email: mail@robfors.com
|
31
30
|
executables: []
|
32
31
|
extensions: []
|
@@ -37,26 +36,29 @@ files:
|
|
37
36
|
- Rakefile
|
38
37
|
- lib/quack_concurrency.rb
|
39
38
|
- lib/quack_concurrency/condition_variable.rb
|
39
|
+
- lib/quack_concurrency/condition_variable/waitable.rb
|
40
40
|
- lib/quack_concurrency/error.rb
|
41
41
|
- lib/quack_concurrency/future.rb
|
42
42
|
- lib/quack_concurrency/future/canceled.rb
|
43
43
|
- lib/quack_concurrency/future/complete.rb
|
44
44
|
- lib/quack_concurrency/mutex.rb
|
45
45
|
- lib/quack_concurrency/queue.rb
|
46
|
-
- lib/quack_concurrency/queue/error.rb
|
47
46
|
- lib/quack_concurrency/reentrant_mutex.rb
|
48
|
-
- lib/quack_concurrency/
|
49
|
-
- lib/quack_concurrency/
|
50
|
-
- lib/quack_concurrency/
|
51
|
-
- lib/quack_concurrency/
|
52
|
-
- lib/quack_concurrency/uninterruptible_sleeper.rb
|
47
|
+
- lib/quack_concurrency/safe_condition_variable.rb
|
48
|
+
- lib/quack_concurrency/safe_condition_variable/waitable.rb
|
49
|
+
- lib/quack_concurrency/safe_sleeper.rb
|
50
|
+
- lib/quack_concurrency/sleeper.rb
|
53
51
|
- lib/quack_concurrency/waiter.rb
|
54
|
-
-
|
52
|
+
- spec/condition_variable_spec.rb
|
55
53
|
- spec/future_spec.rb
|
54
|
+
- spec/mutex_spec.rb
|
56
55
|
- spec/queue_spec.rb
|
57
56
|
- spec/reentrant_mutex_spec.rb
|
58
|
-
- spec/
|
57
|
+
- spec/safe_condition_variable_spec.rb
|
58
|
+
- spec/safe_sleeper_spec.rb
|
59
|
+
- spec/sleeper.rb
|
59
60
|
- spec/spec_helper.rb
|
61
|
+
- spec/waiter_spec.rb
|
60
62
|
homepage: https://github.com/robfors/quack_concurrency
|
61
63
|
licenses:
|
62
64
|
- MIT
|
@@ -77,8 +79,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
79
|
version: '0'
|
78
80
|
requirements: []
|
79
81
|
rubyforge_project:
|
80
|
-
rubygems_version: 2.7.
|
82
|
+
rubygems_version: 2.7.8
|
81
83
|
signing_key:
|
82
84
|
specification_version: 4
|
83
|
-
summary: Concurrency tools
|
85
|
+
summary: Concurrency tools with modifiable blocking behaviour.
|
84
86
|
test_files: []
|
@@ -1,139 +0,0 @@
|
|
1
|
-
# not ready yet
|
2
|
-
|
3
|
-
module QuackConcurrency
|
4
|
-
class Semaphore
|
5
|
-
|
6
|
-
# Gets total permit count
|
7
|
-
# @return [Integer]
|
8
|
-
attr_reader :permit_count
|
9
|
-
|
10
|
-
# Creates a new {Semaphore} concurrency tool.
|
11
|
-
# @return [Semaphore]
|
12
|
-
def initialize(permit_count = 1)
|
13
|
-
raise 'not ready yet'
|
14
|
-
@condition_variable = UninterruptibleConditionVariable.new
|
15
|
-
verify_permit_count(permit_count)
|
16
|
-
@permit_count = permit_count
|
17
|
-
@permits_used = 0
|
18
|
-
@mutex = ::ReentrantMutex.new
|
19
|
-
end
|
20
|
-
|
21
|
-
# Check if a permit is available to be released.
|
22
|
-
# @return [Boolean]
|
23
|
-
def permit_available?
|
24
|
-
permits_available >= 1
|
25
|
-
end
|
26
|
-
|
27
|
-
# Counts number of permits available to be released.
|
28
|
-
# @return [Integer]
|
29
|
-
def permits_available
|
30
|
-
@mutex.synchronize do
|
31
|
-
raw_permits_available = @permit_count - @permits_used
|
32
|
-
raw_permits_available.positive? ? raw_permits_available : 0
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
# Returns a permit so it can be released again in the future.
|
37
|
-
# @return [void]
|
38
|
-
def reacquire
|
39
|
-
@mutex.synchronize do
|
40
|
-
raise Error, 'can not reacquire a permit, no permits released right now' if @permits_used == 0
|
41
|
-
@permits_used -= 1
|
42
|
-
@condition_variable.signal if permit_available?
|
43
|
-
end
|
44
|
-
nil
|
45
|
-
end
|
46
|
-
|
47
|
-
# Releases a permit.
|
48
|
-
# @note Will block until a permit is available.
|
49
|
-
# @return [void]
|
50
|
-
def release
|
51
|
-
@mutex.synchronize do
|
52
|
-
@condition_variable.wait(@mutex) unless permit_available?
|
53
|
-
raise 'internal error, invalid state' unless permit_available?
|
54
|
-
@permits_used += 1
|
55
|
-
end
|
56
|
-
nil
|
57
|
-
end
|
58
|
-
|
59
|
-
# Changes the permit count after {Semaphore} has been created.
|
60
|
-
# @raise [Error] if total permit count is reduced and not enough permits are available to remove
|
61
|
-
# @return [void]
|
62
|
-
def set_permit_count(new_permit_count)
|
63
|
-
verify_permit_count(new_permit_count)
|
64
|
-
@mutex.synchronize do
|
65
|
-
remove_permits = @permit_count - new_permit_count
|
66
|
-
if remove_permits.positive? && remove_permits > permits_available
|
67
|
-
raise Error, 'can not set new permit count, not enough permits available to remove right now'
|
68
|
-
end
|
69
|
-
set_permit_count!(new_permit_count)
|
70
|
-
end
|
71
|
-
nil
|
72
|
-
end
|
73
|
-
|
74
|
-
# Changes the permit count after {Semaphore} has been created.
|
75
|
-
# If total permit count is reduced and not enough permits are available to remove,
|
76
|
-
# it will change the count anyway but some permits will need to be reacquired
|
77
|
-
# before any can be released.
|
78
|
-
# @return [void]
|
79
|
-
def set_permit_count!(new_permit_count)
|
80
|
-
verify_permit_count(new_permit_count)
|
81
|
-
@mutex.synchronize do
|
82
|
-
new_permits = new_permit_count - @permit_count
|
83
|
-
if new_permits.positive?
|
84
|
-
new_permits.times { add_permit }
|
85
|
-
else
|
86
|
-
remove_permits = -new_permits
|
87
|
-
remove_permits.times { remove_permit! }
|
88
|
-
end
|
89
|
-
end
|
90
|
-
nil
|
91
|
-
end
|
92
|
-
|
93
|
-
# Releases a permit, runs the block, and reacquires the permit when the block completes.
|
94
|
-
# @return return value from yielded block
|
95
|
-
def synchronize
|
96
|
-
release
|
97
|
-
begin
|
98
|
-
yield
|
99
|
-
ensure
|
100
|
-
reacquire
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
# Attempts to release a permit and returns immediately.
|
105
|
-
# @return [Boolean] returns if the permit was released
|
106
|
-
def try_release
|
107
|
-
@mutex.synchronize do
|
108
|
-
if permit_available?
|
109
|
-
release
|
110
|
-
true
|
111
|
-
else
|
112
|
-
false
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
private
|
118
|
-
|
119
|
-
def add_permit
|
120
|
-
@permit_count += 1
|
121
|
-
@condition_variable.signal if permit_available?
|
122
|
-
nil
|
123
|
-
end
|
124
|
-
|
125
|
-
def remove_permit!
|
126
|
-
@permit_count -= 1
|
127
|
-
raise 'internal error, invalid state' if @permit_count < 0
|
128
|
-
nil
|
129
|
-
end
|
130
|
-
|
131
|
-
def verify_permit_count(permit_count)
|
132
|
-
unless permit_count.is_a?(Integer) && permit_count >= 0
|
133
|
-
raise ArgumentError, "'permit_count' must be a non negative Integer"
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
@@ -1,94 +0,0 @@
|
|
1
|
-
module QuackConcurrency
|
2
|
-
|
3
|
-
# Unlike `::ConditionVariable` {UninterruptibleConditionVariable} will
|
4
|
-
# safely sleep a `Thread`s. Any calls to `Thread#run` directly, will be ignored.
|
5
|
-
class UninterruptibleConditionVariable
|
6
|
-
|
7
|
-
def initialize
|
8
|
-
@waiting_threads_sleepers = []
|
9
|
-
@mutex = ::Mutex.new
|
10
|
-
end
|
11
|
-
|
12
|
-
def any_waiting_threads?
|
13
|
-
waiting_threads_count >= 1
|
14
|
-
end
|
15
|
-
|
16
|
-
def broadcast
|
17
|
-
@mutex.synchronize do
|
18
|
-
signal_next until @waiting_threads_sleepers.empty?
|
19
|
-
end
|
20
|
-
self
|
21
|
-
end
|
22
|
-
|
23
|
-
def signal
|
24
|
-
@mutex.synchronize do
|
25
|
-
signal_next if @waiting_threads_sleepers.any?
|
26
|
-
end
|
27
|
-
self
|
28
|
-
end
|
29
|
-
|
30
|
-
def wait(mutex = nil, timeout = nil)
|
31
|
-
validate_mutex(mutex)
|
32
|
-
if timeout != nil && !timeout.is_a?(Numeric)
|
33
|
-
raise ArgumentError, "'timeout' argument must be nil or a Numeric"
|
34
|
-
end
|
35
|
-
sleeper = UninterruptibleSleeper.for_current
|
36
|
-
@mutex.synchronize { @waiting_threads_sleepers.push(sleeper) }
|
37
|
-
if mutex
|
38
|
-
# ideally we would would check if this Thread can sleep (not the last Thread alive)
|
39
|
-
# before we unlock the mutex, however I am not sure is that can be implemented
|
40
|
-
if mutex.respond_to?(:unlock!)
|
41
|
-
mutex.unlock! { sleep(sleeper, timeout) }
|
42
|
-
else
|
43
|
-
mutex.unlock
|
44
|
-
begin
|
45
|
-
sleep(sleeper, timeout)
|
46
|
-
ensure # rescue a fatal error (eg. only Thread stopped)
|
47
|
-
if mutex.locked?
|
48
|
-
# another Thread locked this before it died
|
49
|
-
# this is not a correct state to be in but I don't know how to fix it
|
50
|
-
# given that there are no other alive Threads then than the ramifications should be minimal
|
51
|
-
else
|
52
|
-
mutex.lock
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
else
|
57
|
-
sleep(sleeper, timeout)
|
58
|
-
end
|
59
|
-
self
|
60
|
-
ensure
|
61
|
-
@mutex.synchronize { @waiting_threads_sleepers.delete(sleeper) }
|
62
|
-
end
|
63
|
-
|
64
|
-
def waiting_threads_count
|
65
|
-
@waiting_threads_sleepers.length
|
66
|
-
end
|
67
|
-
|
68
|
-
private
|
69
|
-
|
70
|
-
# @api private
|
71
|
-
def signal_next
|
72
|
-
next_waiting_thread_sleeper = @waiting_threads_sleepers.shift
|
73
|
-
next_waiting_thread_sleeper.run_thread if next_waiting_thread_sleeper
|
74
|
-
nil
|
75
|
-
end
|
76
|
-
|
77
|
-
# @api private
|
78
|
-
def sleep(sleeper, duration)
|
79
|
-
if duration == nil || duration == Float::INFINITY
|
80
|
-
sleeper.stop_thread
|
81
|
-
else
|
82
|
-
sleeper.sleep_thread(timeout)
|
83
|
-
end
|
84
|
-
nil
|
85
|
-
end
|
86
|
-
|
87
|
-
def validate_mutex(mutex)
|
88
|
-
return if mutex == nil
|
89
|
-
return if mutex.respond_to?(:lock) && (mutex.respond_to?(:unlock) || mutex.respond_to?(:unlock!))
|
90
|
-
raise ArgumentError, "'mutex' must respond to 'lock' and ('unlock' or'unlock!')"
|
91
|
-
end
|
92
|
-
|
93
|
-
end
|
94
|
-
end
|