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