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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ab1347b652fc8daf227009ca005e3d7720b53e7bf25936047630b8b9ba56fcb
4
- data.tar.gz: b0ef0c7084f46c038d6ec4ac91b940a2da738cfdec4b9ecf82ab02d15b0cb98a
3
+ metadata.gz: 7933176e96ff2045aa5e480dcdfc5682bfc4e3e1ce860e9a1810c08a0f1b1024
4
+ data.tar.gz: 7fa030e38a160f8a136198af6f0fada02de1974c81a06576d1b0cb62934ad230
5
5
  SHA512:
6
- metadata.gz: '08e1d499bc5a4007a6649b58786c60693f3ff9b76cfc69ad59905d9f367df1a239dca09bca12685ccc166727ed3de9c2299b52535eb1b0a86e9eeb5264672c68'
7
- data.tar.gz: f30d2a54cdc67b668ab83ad6bfbf18ac60aacdeaf3358d2c54d88be303152a29f9ebb44648e9cd249193c3d9bc02c3e616f69a6b1f65fdd607d4872b8f00e5a7
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](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/quack_concurrency)
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 < ConcurrencyTool
2
+ class Future
3
3
 
4
- # Creates a new +Future+ concurrency tool.
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(duck_types: nil)
9
- classes = setup_duck_types(duck_types)
10
- @condition_variable = classes[:condition_variable].new
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 future.
18
- # @raise [Complete] if the future is already completed
19
- # @return [void] value of the future
20
- def cancel
21
- @mutex.synchronize do
22
- raise Complete if @complete
23
- @complete = true
24
- @condition_variable.broadcast
25
- end
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 future has a value or is canceled.
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 future.
32
+ # Gets the value of the {Future}.
36
33
  # @note This method will block until the future has completed.
37
- # @raise [Canceled] if the future is canceled
38
- # @return value of the future
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
- @condition_variable.wait(@mutex) unless complete?
42
- raise 'internal error, invalid state' unless complete?
43
- raise Canceled unless @value_set
44
- @value
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 future.
49
- # @raise [Complete] if the future has already completed
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 Complete if @complete
55
- @value_set = true
66
+ Kernel.raise(Complete) if @complete
56
67
  @complete = true
57
68
  @value = new_value
58
- @condition_variable.broadcast
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 < ConcurrencyTool
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(duck_types: nil)
11
- classes = setup_duck_types(duck_types)
12
- @condition_variable = classes[:condition_variable].new
13
- @mutex = classes[:mutex].new
14
- @queue = []
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 queue.
16
+ # Removes all objects from the {Queue}.
20
17
  # @return [self]
21
18
  def clear
22
- @mutex.synchronize { @queue = [] }
19
+ @mutex.synchronize { @items.clear }
23
20
  self
24
21
  end
25
22
 
26
- # Closes the queue. A closed queue cannot be re-opened.
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 +true+.
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 queue as usual.
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
- @condition_variable.broadcast
34
+ @waiter.resume_all
38
35
  end
39
36
  self
40
37
  end
41
38
 
42
- # Checks if queue is closed.
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 queue is empty.
45
+ # Checks if {Queue} is empty.
49
46
  # @return [Boolean]
50
47
  def empty?
51
- @queue.empty?
48
+ @items.empty?
52
49
  end
53
50
 
54
- # Returns the length of the queue.
51
+ # Returns the length of the {Queue}.
55
52
  # @return [Integer]
56
53
  def length
57
- @queue.length
54
+ @items.length
58
55
  end
59
56
  alias_method :size, :length
60
57
 
61
- # Returns the number of threads waiting on the queue.
58
+ # Returns the number of threads waiting on the {Queue}.
62
59
  # @return [Integer]
63
60
  def num_waiting
64
- @waiting_count
61
+ @pop_mutex.waiting_threads_count + @waiter.waiting_threads_count
65
62
  end
66
63
 
67
- # Retrieves item from the queue.
68
- # @note If the queue is empty, it will block until an item is available.
69
- # If +non_block+ is true, it will raise {Error} instead.
70
- # @raise {Error} if queue is empty and +non_block+ is true
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
- @mutex.synchronize do
74
- if @waiting_count >= length
75
- return if closed?
76
- raise Error if non_block
77
- @waiting_count += 1
78
- @condition_variable.wait(@mutex)
79
- @waiting_count -= 1
80
- return if closed?
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 queue.
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
- @queue.push(item)
94
- @condition_variable.signal
93
+ @items.push(item)
94
+ @waiter.resume_one
95
95
  end
96
96
  self
97
97
  end