quack_concurrency 0.4.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Yard Docs](
|
10
|
+
[![Yard Docs](https://img.shields.io/badge/yard-docs-blue.svg?style=for-the-badge)](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
|