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