quack_concurrency 0.4.1 → 0.5.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/condition_variable.rb +116 -0
- data/lib/quack_concurrency/future.rb +43 -32
- data/lib/quack_concurrency/mutex.rb +121 -0
- data/lib/quack_concurrency/queue.rb +39 -39
- data/lib/quack_concurrency/reentrant_mutex.rb +92 -67
- data/lib/quack_concurrency/semaphore.rb +7 -7
- data/lib/quack_concurrency/uninterruptible_condition_variable.rb +79 -0
- data/lib/quack_concurrency/uninterruptible_sleeper.rb +73 -0
- data/lib/quack_concurrency/waiter.rb +42 -12
- data/lib/quack_concurrency/yielder.rb +35 -0
- data/lib/quack_concurrency.rb +8 -7
- data/spec/future_spec.rb +1 -34
- data/spec/queue_spec.rb +1 -34
- data/spec/reentrant_mutex_spec.rb +2 -33
- data/spec/semaphore_spec.rb +239 -272
- metadata +7 -4
- data/lib/quack_concurrency/concurrency_tool.rb +0 -28
- data/lib/quack_concurrency/name.rb +0 -33
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7933176e96ff2045aa5e480dcdfc5682bfc4e3e1ce860e9a1810c08a0f1b1024
|
4
|
+
data.tar.gz: 7fa030e38a160f8a136198af6f0fada02de1974c81a06576d1b0cb62934ad230
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4b9c81683dae54bc82d8db1d9f551635d18bdb0028135483ddaacdfbffdde91f1295d5f5b22498c92ff85bab4fcb1c691cbca61d594bc5d5c62b76503d23842
|
7
|
+
data.tar.gz: 3b474d262f7fa0227f6aa7c25488fb2bae135c5f0ebe4372a79000745143b0e593faaf807e200a1b52309a95cb664e66f580a077af1233c924360e44cb578181
|
data/README.md
CHANGED
@@ -7,4 +7,4 @@ This Ruby Gem offers a few concurrency tools that could also be found in [*Concu
|
|
7
7
|
Then simply `require 'quack_concurrency'` in your project.
|
8
8
|
|
9
9
|
# Documentation
|
10
|
-
[](http://www.rubydoc.info/gems/quack_concurrency)
|
@@ -0,0 +1,116 @@
|
|
1
|
+
module QuackConcurrency
|
2
|
+
|
3
|
+
# @note duck type for `::Thread::ConditionVariable`
|
4
|
+
class ConditionVariable
|
5
|
+
|
6
|
+
# Creates a new {ConditionVariable} concurrency tool.
|
7
|
+
# @return [ConditionVariable]
|
8
|
+
def initialize
|
9
|
+
@waiting_threads = []
|
10
|
+
@mutex = ::Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns if any `Threads` are currently waiting.
|
14
|
+
# @api private
|
15
|
+
# @return [Boolean]
|
16
|
+
def any_waiting_threads?
|
17
|
+
waiting_threads_count >= 1
|
18
|
+
end
|
19
|
+
|
20
|
+
# Wakes up all `Threads` currently waiting.
|
21
|
+
# @return [self]
|
22
|
+
def broadcast
|
23
|
+
@mutex.synchronize do
|
24
|
+
signal_next until @waiting_threads.empty?
|
25
|
+
end
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
# Wakes up the next waiting `Thread`, if any exist.
|
30
|
+
# @return [self]
|
31
|
+
def signal
|
32
|
+
@mutex.synchronize do
|
33
|
+
signal_next if @waiting_threads.any?
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Sleeps the current `Thread`.
|
39
|
+
# @param duration [nil, Numeric] time to sleep in seconds
|
40
|
+
# @api private
|
41
|
+
# @return [void]
|
42
|
+
def sleep(duration)
|
43
|
+
if duration == nil || duration == Float::INFINITY
|
44
|
+
Thread.stop
|
45
|
+
else
|
46
|
+
Thread.sleep(duration)
|
47
|
+
end
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sleeps the current `Thread` until {#signal} or {#broadcast} wake it.
|
52
|
+
# If a {Mutex} is given, the {Mutex} will be unlocked before sleeping and relocked when woken.
|
53
|
+
# @raise [ArgumentError]
|
54
|
+
# @param mutex [nil,Mutex]
|
55
|
+
# @param timeout [nil,Numeric] maximum time to wait, specified in seconds
|
56
|
+
# @return [self]
|
57
|
+
def wait(mutex = nil, timeout = nil)
|
58
|
+
validate_mutex(mutex)
|
59
|
+
if timeout != nil && !timeout.is_a?(Numeric)
|
60
|
+
raise ArgumentError, "'timeout' argument must be nil or a Numeric"
|
61
|
+
end
|
62
|
+
@mutex.synchronize { @waiting_threads.push(caller) }
|
63
|
+
if mutex
|
64
|
+
if mutex.respond_to?(:unlock!)
|
65
|
+
mutex.unlock! { sleep(timeout) }
|
66
|
+
else
|
67
|
+
mutex.unlock
|
68
|
+
sleep(timeout)
|
69
|
+
mutex.lock
|
70
|
+
end
|
71
|
+
else
|
72
|
+
sleep(timeout)
|
73
|
+
end
|
74
|
+
@mutex.synchronize { @waiting_threads.delete(caller) }
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the number of `Thread`s currently waiting.
|
79
|
+
# @api private
|
80
|
+
# @return [Integer]
|
81
|
+
def waiting_threads_count
|
82
|
+
@waiting_threads_sleepers.length
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Gets the currently executing `Thread`.
|
88
|
+
# @api private
|
89
|
+
# @return [Thread]
|
90
|
+
def caller
|
91
|
+
Thread.current
|
92
|
+
end
|
93
|
+
|
94
|
+
# Wakes up the next waiting `Thread`.
|
95
|
+
# Will try again if the `Thread` has already been woken.
|
96
|
+
# @api private
|
97
|
+
# @return [void]
|
98
|
+
def signal_next
|
99
|
+
begin
|
100
|
+
next_waiting_thread = @waiting_threads.shift
|
101
|
+
next_waiting_thread.run if next_waiting_thread
|
102
|
+
rescue ThreadError
|
103
|
+
# Thread must be dead
|
104
|
+
retry
|
105
|
+
end
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate_mutex(mutex)
|
110
|
+
return if mutex == nil
|
111
|
+
return if mutex.respond_to?(:lock) && (mutex.respond_to?(:unlock) || mutex.respond_to?(:unlock!))
|
112
|
+
raise ArgumentError, "'mutex' must respond to 'lock' and ('unlock' or'unlock!')"
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
@@ -1,61 +1,72 @@
|
|
1
1
|
module QuackConcurrency
|
2
|
-
class Future
|
2
|
+
class Future
|
3
3
|
|
4
|
-
# Creates a new
|
5
|
-
# @param duck_types [Hash] hash of core Ruby classes to overload.
|
6
|
-
# If a +Hash+ is given, the keys +:condition_variable+ and +:mutex+ must be present.
|
4
|
+
# Creates a new {Future} concurrency tool.
|
7
5
|
# @return [Future]
|
8
|
-
def initialize
|
9
|
-
|
10
|
-
@
|
11
|
-
@mutex = classes[:mutex].new
|
6
|
+
def initialize
|
7
|
+
@waiter = Waiter.new
|
8
|
+
@mutex = ::Mutex.new
|
12
9
|
@value = nil
|
13
|
-
@value_set = false
|
14
10
|
@complete = false
|
11
|
+
@exception = false
|
15
12
|
end
|
16
13
|
|
17
|
-
# Cancels the
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
14
|
+
# Cancels the {Future}.
|
15
|
+
# Calling {#get} will result in Canceled being raised.
|
16
|
+
# Same as `raise(Canceled.new)`.
|
17
|
+
# @raise [Complete] if the {Future} was already completed
|
18
|
+
# @param exception [Exception] custom `Exception` to set
|
19
|
+
# @return [void]
|
20
|
+
def cancel(exception = nil)
|
21
|
+
exception ||= Canceled.new
|
22
|
+
self.raise(exception)
|
26
23
|
nil
|
27
24
|
end
|
28
25
|
|
29
|
-
# Checks if
|
26
|
+
# Checks if {Future} has a value or was canceled.
|
30
27
|
# @return [Boolean]
|
31
28
|
def complete?
|
32
29
|
@complete
|
33
30
|
end
|
34
31
|
|
35
|
-
# Gets the value of the
|
32
|
+
# Gets the value of the {Future}.
|
36
33
|
# @note This method will block until the future has completed.
|
37
|
-
# @raise [Canceled] if the
|
38
|
-
# @
|
34
|
+
# @raise [Canceled] if the {Future} is canceled
|
35
|
+
# @raise [Exception] if the {Future} was canceled with a given exception
|
36
|
+
# @return [Object] value of the {Future}
|
39
37
|
def get
|
38
|
+
@waiter.wait
|
39
|
+
Kernel.raise(@exception) if @exception
|
40
|
+
@value
|
41
|
+
end
|
42
|
+
|
43
|
+
# Cancels the {Future} with a custom `Exception`.
|
44
|
+
# @raise [Complete] if the future has already completed
|
45
|
+
# @param exception [Exception]
|
46
|
+
# @return [void]
|
47
|
+
def raise(exception = nil)
|
48
|
+
unless exception == nil || exception.is_a?(Exception)
|
49
|
+
Kernel.raise(ArgumentError, "'exception' must be nil or an instance of an Exception")
|
50
|
+
end
|
40
51
|
@mutex.synchronize do
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
@
|
52
|
+
Kernel.raise(Complete) if @complete
|
53
|
+
@complete = true
|
54
|
+
@exception = exception || StandardError.new
|
55
|
+
@waiter.resume_all_forever
|
45
56
|
end
|
57
|
+
nil
|
46
58
|
end
|
47
59
|
|
48
|
-
# Sets the value of the
|
49
|
-
# @raise [Complete] if the
|
50
|
-
# @param new_value value to assign to future
|
60
|
+
# Sets the value of the {Future}.
|
61
|
+
# @raise [Complete] if the {Future} has already completed
|
62
|
+
# @param new_value [nil,Object] value to assign to future
|
51
63
|
# @return [void]
|
52
64
|
def set(new_value = nil)
|
53
65
|
@mutex.synchronize do
|
54
|
-
raise
|
55
|
-
@value_set = true
|
66
|
+
Kernel.raise(Complete) if @complete
|
56
67
|
@complete = true
|
57
68
|
@value = new_value
|
58
|
-
@
|
69
|
+
@waiter.resume_all_forever
|
59
70
|
end
|
60
71
|
nil
|
61
72
|
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
module QuackConcurrency
|
2
|
+
|
3
|
+
# @note duck type for `::Thread::Mutex`
|
4
|
+
class Mutex
|
5
|
+
|
6
|
+
# Creates a new {Mutex} concurrency tool.
|
7
|
+
# @return [Mutex]
|
8
|
+
def initialize
|
9
|
+
@mutex = ::Mutex.new
|
10
|
+
@condition_variable = UninterruptibleConditionVariable.new
|
11
|
+
@owner = nil
|
12
|
+
end
|
13
|
+
|
14
|
+
# @raise [ThreadError] if current `Thread` is already locking it
|
15
|
+
#@overload lock
|
16
|
+
# Obtains the lock or sleeps the current `Thread` until it is available.
|
17
|
+
# @return [void]
|
18
|
+
#@overload lock(&block)
|
19
|
+
# Obtains the lock, runs the block, then releases the lock when the block completes.
|
20
|
+
# @yield block to run with the lock
|
21
|
+
# @return [Object] result of the block
|
22
|
+
def lock(&block)
|
23
|
+
raise ThreadError, 'this Thread is already locking this Mutex' if owned?
|
24
|
+
if block_given?
|
25
|
+
lock
|
26
|
+
begin
|
27
|
+
yield
|
28
|
+
ensure
|
29
|
+
unlock
|
30
|
+
end
|
31
|
+
else
|
32
|
+
@mutex.synchronize do
|
33
|
+
@condition_variable.wait(@mutex) if locked?
|
34
|
+
@owner = caller
|
35
|
+
end
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def locked?
|
41
|
+
!!@owner
|
42
|
+
end
|
43
|
+
|
44
|
+
def owned?
|
45
|
+
@owner == caller
|
46
|
+
end
|
47
|
+
|
48
|
+
def owner
|
49
|
+
@owner
|
50
|
+
end
|
51
|
+
|
52
|
+
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
|
56
|
+
unlock do
|
57
|
+
if timeout
|
58
|
+
elapsed_time = Kernel.sleep(timeout)
|
59
|
+
else
|
60
|
+
elapsed_time = Kernel.sleep
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def synchronize(&block)
|
66
|
+
lock(&block)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Attempts to obtain the lock and returns immediately.
|
70
|
+
# @return [Boolean] returns if the lock was granted
|
71
|
+
def try_lock
|
72
|
+
raise ThreadError, 'this Thread is already locking this Mutex' if owned?
|
73
|
+
@mutex.synchronize do
|
74
|
+
if locked?
|
75
|
+
false
|
76
|
+
else
|
77
|
+
@owner = caller
|
78
|
+
true
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def unlock(&block)
|
84
|
+
if block_given?
|
85
|
+
unlock
|
86
|
+
begin
|
87
|
+
yield
|
88
|
+
ensure
|
89
|
+
lock
|
90
|
+
end
|
91
|
+
else
|
92
|
+
@mutex.synchronize do
|
93
|
+
raise ThreadError, 'Mutex is not locked' unless locked?
|
94
|
+
raise ThreadError, 'current Thread is not locking the Mutex' unless owned?
|
95
|
+
if @condition_variable.any_waiting_threads?
|
96
|
+
@condition_variable.signal
|
97
|
+
|
98
|
+
# 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
|
101
|
+
@owner = true
|
102
|
+
else
|
103
|
+
@owner = nil
|
104
|
+
end
|
105
|
+
end
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def waiting_threads_count
|
111
|
+
@condition_variable.waiting_threads_count
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def caller
|
117
|
+
Thread.current
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
@@ -1,97 +1,97 @@
|
|
1
1
|
module QuackConcurrency
|
2
2
|
|
3
3
|
# @note duck type for +::Thread::Queue+
|
4
|
-
class Queue
|
4
|
+
class Queue
|
5
5
|
|
6
6
|
# Creates a new {Queue} concurrency tool.
|
7
|
-
# @param duck_types [Hash] hash of core Ruby classes to overload.
|
8
|
-
# If a +Hash+ is given, the keys +:condition_variable+ and +:mutex+ must be present.
|
9
7
|
# @return [Queue]
|
10
|
-
def initialize
|
11
|
-
|
12
|
-
@
|
13
|
-
@
|
14
|
-
@
|
15
|
-
@waiting_count = 0
|
8
|
+
def initialize
|
9
|
+
@mutex = ::Mutex.new
|
10
|
+
@pop_mutex = Mutex.new
|
11
|
+
@waiter = Waiter.new
|
12
|
+
@items = []
|
16
13
|
@closed = false
|
17
14
|
end
|
18
15
|
|
19
|
-
# Removes all objects from the
|
16
|
+
# Removes all objects from the {Queue}.
|
20
17
|
# @return [self]
|
21
18
|
def clear
|
22
|
-
@mutex.synchronize { @
|
19
|
+
@mutex.synchronize { @items.clear }
|
23
20
|
self
|
24
21
|
end
|
25
22
|
|
26
|
-
# Closes the
|
23
|
+
# Closes the {Queue}. A closed {Queue} cannot be re-opened.
|
27
24
|
# After the call to close completes, the following are true:
|
28
|
-
# * {#closed?} will return
|
25
|
+
# * {#closed?} will return `true`.
|
29
26
|
# * {#close} will be ignored.
|
30
27
|
# * {#push} will raise an exception.
|
31
|
-
# * until empty, calling {#pop} will return an object from the
|
28
|
+
# * until empty, calling {#pop} will return an object from the {Queue} as usual.
|
32
29
|
# @return [self]
|
33
30
|
def close
|
34
31
|
@mutex.synchronize do
|
35
32
|
return if closed?
|
36
33
|
@closed = true
|
37
|
-
@
|
34
|
+
@waiter.resume_all
|
38
35
|
end
|
39
36
|
self
|
40
37
|
end
|
41
38
|
|
42
|
-
# Checks if
|
39
|
+
# Checks if {Queue} is closed.
|
43
40
|
# @return [Boolean]
|
44
41
|
def closed?
|
45
42
|
@closed
|
46
43
|
end
|
47
44
|
|
48
|
-
# Checks if
|
45
|
+
# Checks if {Queue} is empty.
|
49
46
|
# @return [Boolean]
|
50
47
|
def empty?
|
51
|
-
@
|
48
|
+
@items.empty?
|
52
49
|
end
|
53
50
|
|
54
|
-
# Returns the length of the
|
51
|
+
# Returns the length of the {Queue}.
|
55
52
|
# @return [Integer]
|
56
53
|
def length
|
57
|
-
@
|
54
|
+
@items.length
|
58
55
|
end
|
59
56
|
alias_method :size, :length
|
60
57
|
|
61
|
-
# Returns the number of threads waiting on the
|
58
|
+
# Returns the number of threads waiting on the {Queue}.
|
62
59
|
# @return [Integer]
|
63
60
|
def num_waiting
|
64
|
-
@
|
61
|
+
@pop_mutex.waiting_threads_count + @waiter.waiting_threads_count
|
65
62
|
end
|
66
63
|
|
67
|
-
# Retrieves item from the
|
68
|
-
# @note If the
|
69
|
-
# If
|
70
|
-
# @raise {
|
71
|
-
# @param non_block [Boolean]
|
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`
|
68
|
+
# @param non_block [Boolean]
|
72
69
|
def pop(non_block = false)
|
73
|
-
@
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
70
|
+
@pop_mutex.lock do
|
71
|
+
@mutex.synchronize do
|
72
|
+
if empty?
|
73
|
+
return if closed?
|
74
|
+
raise ThreadError if non_block
|
75
|
+
@mutex.unlock
|
76
|
+
@waiter.wait
|
77
|
+
@mutex.lock
|
78
|
+
return if closed?
|
79
|
+
end
|
80
|
+
@items.shift
|
81
81
|
end
|
82
|
-
@queue.shift
|
83
82
|
end
|
84
83
|
end
|
85
84
|
alias_method :deq, :pop
|
86
85
|
alias_method :shift, :pop
|
87
86
|
|
88
|
-
# Pushes the given object to the
|
87
|
+
# Pushes the given object to the {Queue}.
|
88
|
+
# @param item [Object]
|
89
89
|
# @return [self]
|
90
90
|
def push(item = nil)
|
91
91
|
@mutex.synchronize do
|
92
92
|
raise ClosedQueueError if closed?
|
93
|
-
@
|
94
|
-
@
|
93
|
+
@items.push(item)
|
94
|
+
@waiter.resume_one
|
95
95
|
end
|
96
96
|
self
|
97
97
|
end
|