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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d110c1994e7796f45c3cade0f82f7bae2ce8daa0fa775e32fe92cb5a5d1bb3e7
4
- data.tar.gz: 1253c01f31d051a5cdccd599c3d1ce597bc580c9e06311727b132aa4e1f2e172
3
+ metadata.gz: 98f80adbcb27205b31915dac49d3577b27f9fe57c1c47e54950d1484c6788b90
4
+ data.tar.gz: f43c9afe0d281b1a739533db8aa6631808ceada3d5712447cc9d35ec3e25a07e
5
5
  SHA512:
6
- metadata.gz: c44997c7c973429240bbf600ea68818eedb548012ccc0248028a4fc5a18509e9050b44baa92c563190512c1cb34a0999d8624288827915eb4dcdbc05997961ad
7
- data.tar.gz: 99f22237f7a44ea60ac10c0a25c8ac6627fa17cf13de2f55e4797a749607f676dd02ffbfd47ade2f93a983e5ca83dab89e8374480ab930326154e47c14d69c36
6
+ metadata.gz: 8ccbd4e65d61e1a69785d5a35e14e5cb77357260c15958de82abbd3b79567b2cbd3a19667a5f25a7523e55aefb744447473e95bfc60fa19c009bcc1d98932682
7
+ data.tar.gz: f45e1759342961393e890d8bcb34d5a244ba70eba8c8a5ae7671886692330579b07c9890764d41ad7c442dbe3d1a3a9141b6a26022914430564c154febc18f7b
data/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Quack Concurrency
2
- This Ruby Gem offers a few concurrency tools that could also be found in [*Concurrent Ruby*](https://github.com/ruby-concurrency/concurrent-ruby). However, all of *Quack Concurrency's* tools will tolerate duck types of Ruby's core classes to adjust the blocking behaviour of the tools. The tools include: `ConditionVariable`, `Future`, `Mutex`, `Queue`, `ReentrantMutex`, `UninterruptibleConditionVariable` and `UninterruptibleSleeper`. The tools will accept duck types for `Thread` and `Kernel`. *TODO: list some projects useing it*.
2
+ This Ruby Gem offers a few concurrency tools that could also be found in [*Concurrent Ruby*](https://github.com/ruby-concurrency/concurrent-ruby). However, all of *Quack Concurrency's* tools will tolerate duck types of Ruby's core `::Mutex` and `::ConditionVariable` classes to adjust the blocking behaviour of the tools. The tools available include: `ConditionVariable`, `Future`, `Mutex`, `Queue`, `ReentrantMutex`, `SafeConditionVariable`, `SafeSleeper` and `Sleeper`. *TODO: list some projects using it*.
3
3
 
4
4
  # Install
5
5
  `gem install quack_concurrency`
@@ -2,28 +2,21 @@ require 'thread'
2
2
  require 'reentrant_mutex'
3
3
 
4
4
  require 'quack_concurrency/condition_variable'
5
+ require 'quack_concurrency/condition_variable/waitable'
5
6
  require 'quack_concurrency/error'
6
7
  require 'quack_concurrency/future'
7
8
  require 'quack_concurrency/future/canceled'
8
9
  require 'quack_concurrency/future/complete'
9
10
  require 'quack_concurrency/mutex'
10
11
  require 'quack_concurrency/queue'
11
- require 'quack_concurrency/queue/error'
12
12
  require 'quack_concurrency/reentrant_mutex'
13
- require 'quack_concurrency/reentrant_mutex/error'
14
- require 'quack_concurrency/uninterruptible_condition_variable'
15
- require 'quack_concurrency/uninterruptible_sleeper'
13
+ require 'quack_concurrency/safe_condition_variable'
14
+ require 'quack_concurrency/safe_condition_variable/waitable'
15
+ require 'quack_concurrency/sleeper'
16
+ require 'quack_concurrency/safe_sleeper'
16
17
  require 'quack_concurrency/waiter'
17
18
 
18
19
 
19
- # if you pass a duck type Hash to any of the concurrency tools it will force you to
20
- # supply all the required ducktypes, all or nothing, as it were
21
- # this is to protect against forgetting to pass one of the duck types as this
22
- # would be a hard bug to solve otherwise
23
-
24
-
25
20
  module QuackConcurrency
26
-
27
- ClosedQueueError = ::ClosedQueueError
28
-
21
+
29
22
  end
@@ -1,128 +1,134 @@
1
1
  module QuackConcurrency
2
-
3
- # @note duck type for `::Thread::ConditionVariable`
2
+
3
+ # {ConditionVariable} is similar to +::ConditionVariable+.
4
+ #
5
+ # A a few differences include:
6
+ # * {#wait} supports passing a {ReentrantMutex} and {Mutex}
7
+ # * methods have been added to get information on waiting threads
4
8
  class ConditionVariable
5
-
9
+
6
10
  # Creates a new {ConditionVariable} concurrency tool.
7
11
  # @return [ConditionVariable]
8
12
  def initialize
9
- @waiting_threads = []
10
13
  @mutex = ::Mutex.new
14
+ @waitables = []
15
+ @waitables_to_resume = []
11
16
  end
12
-
13
- # Returns if any `Threads` are currently waiting.
14
- # @api private
17
+
18
+ # Checks if any threads are waiting on it.
15
19
  # @return [Boolean]
16
20
  def any_waiting_threads?
17
21
  waiting_threads_count >= 1
18
22
  end
19
-
20
- # Wakes up all `Threads` currently waiting.
23
+
24
+ # Resumes all threads waiting on it.
21
25
  # @return [self]
22
26
  def broadcast
23
27
  @mutex.synchronize do
24
- signal_next until @waiting_threads.empty?
28
+ signal_next until @waitables_to_resume.empty?
25
29
  end
26
30
  self
27
31
  end
28
-
29
- # Wakes up the next waiting `Thread`, if any exist.
32
+
33
+ # Returns the {Waitable} representing the next thread to be woken.
34
+ # It will return the thread that made the earliest call to {#wait}.
35
+ # @api private
36
+ # @return [Waitable]
37
+ def next_waitable_to_wake
38
+ @mutex.synchronize { @waitables.first }
39
+ end
40
+
41
+ # Resumes next thread waiting on it if one exists.
30
42
  # @return [self]
31
43
  def signal
32
44
  @mutex.synchronize do
33
- signal_next if @waiting_threads.any?
45
+ signal_next if @waitables_to_resume.any?
34
46
  end
35
47
  self
36
48
  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
49
+
50
+ # Puts this thread to sleep until another thread resumes it.
51
+ # Threads will be woken in the chronological order that this was called.
52
+ # @note Will block until resumed
53
+ # @param mutex [Mutex] mutex to be unlocked while this thread is sleeping
54
+ # @param timeout [nil,Numeric] maximum time to sleep in seconds, +nil+ for forever
55
+ # @raise [TypeError] if +timeout+ is not +nil+ or +Numeric+
56
+ # @raise [ArgumentError] if +timeout+ is negative
57
+ # @raise [Exception] any exception raised by +::ConditionVariable#wait+ (eg. interrupts, +ThreadError+)
56
58
  # @return [self]
57
- def wait(mutex = nil, timeout = nil)
59
+ def wait(mutex, timeout = nil)
58
60
  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
- # ideally we would would check if this Thread can sleep (not the last Thread alive)
65
- # before we unlock the mutex, however I am not sure is that can be implemented
66
- if mutex.respond_to?(:unlock!)
67
- mutex.unlock! { sleep(timeout) }
68
- else
69
- mutex.unlock
70
- begin
71
- sleep(timeout)
72
- ensure # rescue a fatal error (eg. only Thread stopped)
73
- if mutex.locked?
74
- # another Thread locked this before it died
75
- # this is not a correct state to be in but I don't know how to fix it
76
- # given that there are no other alive Threads then than the ramifications should be minimal
77
- else
78
- mutex.lock
79
- end
80
- end
81
- end
82
- else
83
- sleep(timeout)
61
+ validate_timeout(timeout)
62
+ waitable = waitable_for_current_thread
63
+ @mutex.synchronize do
64
+ @waitables.push(waitable)
65
+ @waitables_to_resume.push(waitable)
84
66
  end
67
+ waitable.wait(mutex, timeout)
85
68
  self
86
- ensure
87
- @mutex.synchronize { @waiting_threads.delete(caller) }
88
69
  end
89
-
90
- # Returns the number of `Thread`s currently waiting.
70
+
71
+ # Remove a {Waitable} whose thread has been woken.
91
72
  # @api private
73
+ # @return [void]
74
+ def waitable_woken(waitable)
75
+ @mutex.synchronize { @waitables.delete(waitable) }
76
+ end
77
+
78
+ # Returns the number of threads currently waiting on it.
92
79
  # @return [Integer]
93
80
  def waiting_threads_count
94
- @waiting_threads_sleepers.length
81
+ @waitables.length
95
82
  end
96
-
83
+
97
84
  private
98
-
99
- # Gets the currently executing `Thread`.
100
- # @api private
101
- # @return [Thread]
102
- def caller
103
- Thread.current
104
- end
105
-
106
- # Wakes up the next waiting `Thread`.
107
- # Will try again if the `Thread` has already been woken.
85
+
86
+ # Wakes up the next waiting thread.
87
+ # Will try again if the thread has already been woken.
108
88
  # @api private
109
89
  # @return [void]
110
90
  def signal_next
111
- begin
112
- next_waiting_thread = @waiting_threads.shift
113
- next_waiting_thread.run if next_waiting_thread
114
- rescue ThreadError
115
- # Thread must be dead
116
- retry
91
+ loop do
92
+ next_waitable = @waitables_to_resume.shift
93
+ if next_waitable
94
+ resume_successful = next_waitable.resume
95
+ break if resume_successful
96
+ end
117
97
  end
118
98
  nil
119
99
  end
120
-
100
+
101
+ # Validates that an object behaves like a +::Mutex+
102
+ # Must be able to lock and unlock +mutex+.
103
+ # @api private
104
+ # @param mutex [Mutex] mutex to be validated
105
+ # @raise [TypeError] if +mutex+ does not behave like a +::Mutex+
106
+ # @return [void]
121
107
  def validate_mutex(mutex)
122
- return if mutex == nil
123
- return if mutex.respond_to?(:lock) && (mutex.respond_to?(:unlock) || mutex.respond_to?(:unlock!))
124
- raise ArgumentError, "'mutex' must respond to 'lock' and ('unlock' or'unlock!')"
108
+ return if mutex.respond_to?(:lock) && mutex.respond_to?(:unlock)
109
+ return if mutex.respond_to?(:unlock!)
110
+ raise TypeError, "'mutex' must respond to ('lock' and 'unlock') or 'unlock!'"
111
+ end
112
+
113
+ # Validates a timeout value
114
+ # @api private
115
+ # @param timeout [nil,Numeric]
116
+ # @raise [TypeError] if +timeout+ is not +nil+ or +Numeric+
117
+ # @raise [ArgumentError] if +timeout+ is negative
118
+ # @return [void]
119
+ def validate_timeout(timeout)
120
+ unless timeout == nil
121
+ raise TypeError, "'timeout' must be nil or a Numeric" unless timeout.is_a?(Numeric)
122
+ raise ArgumentError, "'timeout' must not be negative" if timeout.negative?
123
+ end
124
+ end
125
+
126
+ # Returns a waitable to represent the current thread.
127
+ # @api private
128
+ # @return [Waitable]
129
+ def waitable_for_current_thread
130
+ Waitable.new(self)
125
131
  end
126
-
132
+
127
133
  end
128
134
  end
@@ -0,0 +1,108 @@
1
+ module QuackConcurrency
2
+ class ConditionVariable
3
+
4
+ # Used to put threads to sleep and wake them back up in order.
5
+ # A given mutex will be unlocked while the thread sleeps.
6
+ # When waking a thread it will ensure the mutex is relocked before wakng the next thread.
7
+ # Threads will be woken in the chronological order that {#wait} was called.
8
+ class Waitable
9
+
10
+ # Creates a new {Waitable}.
11
+ # @return [ConditionVariable]
12
+ def initialize(condition_variable)
13
+ @condition_variable = condition_variable
14
+ @complete_condition_variable = ::ConditionVariable.new
15
+ @mutex = ::Mutex.new
16
+ @sleeper = Sleeper.new
17
+ @state = :inital
18
+ end
19
+
20
+ # Request the sleeping thread to wake.
21
+ # It will return +false+ if the thread was already woken,
22
+ # possibly due to an interrupt or calling +Thread#run+, etc.
23
+ # @return [Boolean] if the thread was successfully woken during this call
24
+ def resume
25
+ @mutex.synchronize do
26
+ if @state == :complete
27
+ false
28
+ else
29
+ @sleeper.wake
30
+ true
31
+ end
32
+ end
33
+ end
34
+
35
+ # Puts this thread to sleep until {#resume} is called.
36
+ # Unlocks +mutex+ while sleeping
37
+ # It will ensure that previous sleeping threads have resumed before mutex is relocked.
38
+ # @note Will block until resumed
39
+ # @param mutex [Mutex] mutex to be unlocked while this thread is sleeping
40
+ # @param timeout [nil,Numeric] maximum time to sleep in seconds, nil for forever
41
+ # @raise [TypeError] if +timeout+ is not +nil+ or +Numeric+
42
+ # @raise [ArgumentError] if +timeout+ is negative
43
+ # @raise [Exception] any exception raised by +::ConditionVariable#wait+ (eg. interrupts, +ThreadError+)
44
+ # @return [self]
45
+ def wait(mutex, timeout)
46
+ # ideally we would would check if this thread can sleep (ie. is not the last thread alive)
47
+ # before we unlock the mutex, however I am not sure that it can be implemented
48
+ if mutex.respond_to?(:unlock!)
49
+ mutex.unlock! { sleep(timeout) }
50
+ else
51
+ mutex_unlock(mutex) { sleep(timeout) }
52
+ end
53
+ ensure
54
+ @mutex.synchronize do
55
+ @condition_variable.waitable_woken(self)
56
+ @state = :complete
57
+ @complete_condition_variable.broadcast
58
+ end
59
+ end
60
+
61
+ # Wait until thread has woken and relocked the mutex.
62
+ # Will block until thread has resumed.
63
+ # Will not block if {#resume} has already been called.
64
+ # @api private
65
+ # @return [void]
66
+ def wait_until_resumed
67
+ @mutex.synchronize do
68
+ @complete_condition_variable.wait(@mutex) unless @state == :complete
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ # Temporarily unlocks a mutex while a block is run.
75
+ # If an error is raised in the block, +mutex+ will try to be immediately relocked
76
+ # before passing the error up. If unsuccessful, a +ThreadError+ will be raised to
77
+ # imitate the core's behavior.
78
+ # @api private
79
+ # @raise [ThreadError] if relock unsuccessful after an error
80
+ # @return [void]
81
+ def mutex_unlock(mutex, &block)
82
+ mutex.unlock
83
+ yield
84
+ mutex.lock
85
+ rescue Exception
86
+ unless mutex.try_lock
87
+ raise ThreadError, "Attempt to lock a mutex which is locked by another thread"
88
+ end
89
+ raise
90
+ end
91
+
92
+ # Puts this thread to sleep.
93
+ # It will ensure that previous sleeping threads have resumed before returning.
94
+ # @api private
95
+ # @param timeout [nil, Numeric] time to sleep in seconds, nil for forever
96
+ # @return [void]
97
+ def sleep(timeout)
98
+ @sleeper.sleep(timeout)
99
+ loop do
100
+ next_waitable = @condition_variable.next_waitable_to_wake
101
+ break if next_waitable == self
102
+ next_waitable.wait_until_resumed
103
+ end
104
+ end
105
+
106
+ end
107
+ end
108
+ end
@@ -1,5 +1,5 @@
1
1
  module QuackConcurrency
2
2
  class Error < StandardError
3
+
3
4
  end
4
5
  end
5
-
@@ -1,68 +1,69 @@
1
1
  module QuackConcurrency
2
+
3
+ # Used to send a value or error from one thread to another without the need for coordination.
2
4
  class Future
3
-
5
+
4
6
  # Creates a new {Future} concurrency tool.
5
7
  # @return [Future]
6
8
  def initialize
7
- @waiter = Waiter.new
8
- @mutex = ::Mutex.new
9
- @value = nil
10
9
  @complete = false
11
10
  @exception = false
11
+ @mutex = ::Mutex.new
12
+ @value = nil
13
+ @waiter = Waiter.new
12
14
  end
13
-
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
15
+
16
+ # Cancels it.
17
+ # If no +exception+ is specified, a {Canceled} error will be set.
18
+ # @raise [Complete] if the {Future} has already completed
19
+ # @param exception [Exception] custom exception to set (see {#raise})
19
20
  # @return [void]
20
21
  def cancel(exception = nil)
21
22
  exception ||= Canceled.new
22
23
  self.raise(exception)
23
24
  nil
24
25
  end
25
-
26
- # Checks if {Future} has a value or was canceled.
26
+
27
+ # Checks if it has a value or error set.
27
28
  # @return [Boolean]
28
29
  def complete?
29
30
  @complete
30
31
  end
31
-
32
- # Gets the value of the {Future}.
33
- # @note This method will block until the future has completed.
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}
32
+
33
+ # Gets it's value.
34
+ # @note This method will block until the future has completed
35
+ # @raise [Canceled] if it is canceled
36
+ # @raise [Exception] if specific error was set
37
+ # @return [Object] it's value
37
38
  def get
38
39
  @waiter.wait
39
40
  Kernel.raise(@exception) if @exception
40
41
  @value
41
42
  end
42
-
43
- # Cancels the {Future} with a custom `Exception`.
44
- # @raise [Complete] if the future has already completed
45
- # @param exception [Exception]
43
+
44
+ # Sets it to an error.
45
+ # @raise [Complete] if the it has already completed
46
+ # @param exception [nil,Object] +Exception+ class or instance to set, otherwise a +StandardError+ will be set
46
47
  # @return [void]
47
48
  def raise(exception = nil)
48
49
  exception = case
49
50
  when exception == nil then StandardError.new
50
51
  when exception.is_a?(Exception) then exception
51
- when exception <= Exception then exception.new
52
+ when Exception >= exception then exception.new
52
53
  else
53
- Kernel.raise(ArgumentError, "'exception' must be nil or an instance of or descendant of Exception")
54
+ Kernel.raise(TypeError, "'exception' must be nil or an instance of or descendant of Exception")
54
55
  end
55
56
  @mutex.synchronize do
56
57
  Kernel.raise(Complete) if @complete
57
58
  @complete = true
58
59
  @exception = exception
59
- @waiter.resume_all_forever
60
+ @waiter.resume_all_indefinitely
60
61
  end
61
62
  nil
62
63
  end
63
-
64
- # Sets the value of the {Future}.
65
- # @raise [Complete] if the {Future} has already completed
64
+
65
+ # Sets it to a value.
66
+ # @raise [Complete] if it has already completed
66
67
  # @param new_value [nil,Object] value to assign to future
67
68
  # @return [void]
68
69
  def set(new_value = nil)
@@ -70,10 +71,10 @@ module QuackConcurrency
70
71
  Kernel.raise(Complete) if @complete
71
72
  @complete = true
72
73
  @value = new_value
73
- @waiter.resume_all_forever
74
+ @waiter.resume_all_indefinitely
74
75
  end
75
76
  nil
76
77
  end
77
-
78
+
78
79
  end
79
80
  end