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.
@@ -2,113 +2,138 @@
2
2
 
3
3
 
4
4
  module QuackConcurrency
5
- class ReentrantMutex < ConcurrencyTool
5
+ class ReentrantMutex < Mutex
6
6
 
7
7
  # Creates a new {ReentrantMutex} concurrency tool.
8
- # @param duck_types [Hash] hash of core Ruby classes to overload.
9
- # If a +Hash+ is given, the keys +:condition_variable+ and +:mutex+ must be present.
10
8
  # @return [ReentrantMutex]
11
- def initialize(duck_types: nil)
12
- classes = setup_duck_types(duck_types)
13
- @condition_variable = classes[:condition_variable].new
14
- @mutex = classes[:mutex].new
15
- @owner = nil
9
+ def initialize
10
+ super
16
11
  @lock_depth = 0
17
12
  end
18
13
 
19
- # Locks this {ReentrantMutex}. Will block until available.
20
- # @return [void]
21
- def lock
22
- @mutex.synchronize do
23
- @condition_variable.wait(@mutex) if @owner && @owner != caller
24
- raise 'internal error, invalid state' if @owner && @owner != caller
25
- @owner = caller
14
+ #@overload lock
15
+ # Obtains the lock or sleeps the current `Thread` until it is available.
16
+ # @return [void]
17
+ #@overload lock(&block)
18
+ # Obtains the lock, runs the block, then releases the lock when the block completes.
19
+ # @yield block to run with the lock
20
+ # @return [Object] result of the block
21
+ def lock(&block)
22
+ if block_given?
23
+ lock
24
+ start_depth = @lock_depth
25
+ start_owner = owner
26
+ begin
27
+ yield
28
+ ensure
29
+ unless @lock_depth == start_depth && owner == start_owner
30
+ raise Error, 'could not unlock reentrant mutex as its state has been modified'
31
+ end
32
+ unlock
33
+ end
34
+ else
35
+ super unless owned?
26
36
  @lock_depth += 1
37
+ nil
27
38
  end
28
- nil
29
- end
30
-
31
- # Checks if this {ReentrantMutex} is locked by some thread.
32
- # @return [Boolean]
33
- def locked?
34
- !!@owner
35
39
  end
36
40
 
37
- # Checks if this {ReentrantMutex} is locked by a thread other than the caller.
41
+ # Checks if this {ReentrantMutex} is locked by a Thread other than the caller.
38
42
  # @return [Boolean]
39
43
  def locked_out?
40
- @mutex.synchronize { locked? && @owner != caller }
41
- end
42
-
43
- # Checks if this {ReentrantMutex} is locked by the calling thread.
44
- # @return [Boolean]
45
- def owned?
46
- @owner == caller
44
+ # don't need a mutex because we know #owned? can't change during the call
45
+ locked? && !owned?
47
46
  end
48
47
 
49
48
  # Releases the lock and sleeps.
50
- # When the calling thread is next woken up, it will attempt to reacquire the lock.
51
- # @param timeout [Integer] seconds to sleep, +nil+ will sleep forever
52
- # @raise [Error] if this {ReentrantMutex} wasn't locked by the calling thread.
49
+ # When the calling Thread is next woken up, it will attempt to reacquire the lock.
50
+ # @param timeout [Integer] seconds to sleep, `nil` will sleep forever
51
+ # @raise [Error] if this {ReentrantMutex} wasn't locked by the calling Thread
53
52
  # @return [void]
54
53
  def sleep(timeout = nil)
55
- unlock
56
- # i would rather not need to get a ducktype for sleep so we will just take
57
- # advantage of Mutex's sleep method that must take it into account already
58
- @mutex.synchronize do
59
- @mutex.sleep(timeout)
54
+ raise Error, 'can not unlock reentrant mutex, it is not locked' unless locked?
55
+ raise Error, 'can not unlock reentrant mutex, caller is not the owner' unless owned?
56
+ base_depth do
57
+ super(timeout)
60
58
  end
61
- nil
62
- ensure
63
- lock unless owned?
64
59
  end
65
60
 
66
61
  # Obtains a lock, runs the block, and releases the lock when the block completes.
67
- # @return return value from yielded block
68
- def synchronize
69
- lock
70
- start_depth = @lock_depth
71
- start_owner = @owner
72
- result = yield
73
- result
74
- ensure
75
- unless @lock_depth == start_depth && @owner == start_owner
76
- raise Error, 'could not unlock reentrant mutex as its state has been modified'
77
- end
78
- unlock
62
+ # @return [Object] value from yielded block
63
+ def synchronize(&block)
64
+ lock(&block)
79
65
  end
80
66
 
67
+ alias parent_try_lock try_lock
68
+ private :parent_try_lock
81
69
  # Attempts to obtain the lock and returns immediately.
82
70
  # @return [Boolean] returns if the lock was granted
83
71
  def try_lock
84
- @mutex.synchronize do
85
- return false if @owner && @owner != caller
86
- @owner = caller
72
+ if owned?
87
73
  @lock_depth += 1
88
74
  true
75
+ else
76
+ lock_successful = parent_try_lock
77
+ if lock_successful
78
+ @lock_depth += 1
79
+ true
80
+ else
81
+ false
82
+ end
89
83
  end
90
84
  end
91
85
 
92
86
  # Releases the lock.
93
- # @raise [Error] if {ReentrantMutex} wasn't locked by the calling thread
87
+ # @raise [Error] if {ReentrantMutex} wasn't locked by the calling Thread
94
88
  # @return [void]
95
- def unlock
96
- @mutex.synchronize do
97
- raise Error, 'can not unlock reentrant mutex, it is not locked' if @lock_depth == 0
98
- raise Error, 'can not unlock reentrant mutex, caller is not the owner' unless @owner == caller
89
+ def unlock(&block)
90
+ raise Error, 'can not unlock reentrant mutex, it is not locked' unless locked?
91
+ raise Error, 'can not unlock reentrant mutex, caller is not the owner' unless owned?
92
+ if block_given?
93
+ unlock
94
+ begin
95
+ yield
96
+ ensure
97
+ lock
98
+ end
99
+ else
99
100
  @lock_depth -= 1
100
- if @lock_depth == 0
101
- @owner = nil
102
- @condition_variable.signal
101
+ super if @lock_depth == 0
102
+ nil
103
+ end
104
+ end
105
+
106
+ # Releases the lock.
107
+ # @raise [Error] if {ReentrantMutex} wasn't locked by the calling Thread
108
+ # @return [void]
109
+ def unlock!(&block)
110
+ raise Error, 'can not unlock reentrant mutex, it is not locked' unless locked?
111
+ raise Error, 'can not unlock reentrant mutex, caller is not the owner' unless owned?
112
+ if block_given?
113
+ base_depth do
114
+ unlock
115
+ begin
116
+ yield
117
+ ensure
118
+ lock
119
+ end
103
120
  end
121
+ else
122
+ @lock_depth = 0
123
+ super
124
+ nil
104
125
  end
105
- nil
106
126
  end
107
127
 
108
128
  private
109
129
 
110
- def caller
111
- Thread.current
130
+ # @api private
131
+ def base_depth(&block)
132
+ start_depth = @lock_depth
133
+ @lock_depth = 1
134
+ yield
135
+ ensure
136
+ @lock_depth = start_depth
112
137
  end
113
138
 
114
139
  end
@@ -1,21 +1,21 @@
1
+ # not ready yet
2
+
1
3
  module QuackConcurrency
2
- class Semaphore < ConcurrencyTool
4
+ class Semaphore
3
5
 
4
6
  # Gets total permit count
5
7
  # @return [Integer]
6
8
  attr_reader :permit_count
7
9
 
8
10
  # Creates a new {Semaphore} concurrency tool.
9
- # @param duck_types [Hash] hash of core Ruby classes to overload.
10
- # If a +Hash+ is given, the keys +:condition_variable+ and +:mutex+ must be present.
11
11
  # @return [Semaphore]
12
- def initialize(permit_count = 1, duck_types: nil)
13
- classes = setup_duck_types(duck_types)
14
- @condition_variable = classes[:condition_variable].new
12
+ def initialize(permit_count = 1)
13
+ raise 'not ready yet'
14
+ @condition_variable = UninterruptibleConditionVariable.new
15
15
  verify_permit_count(permit_count)
16
16
  @permit_count = permit_count
17
17
  @permits_used = 0
18
- @mutex = ReentrantMutex.new(duck_types: duck_types)
18
+ @mutex = ::ReentrantMutex.new
19
19
  end
20
20
 
21
21
  # Check if a permit is available to be released.
@@ -0,0 +1,79 @@
1
+ module QuackConcurrency
2
+ class UninterruptibleConditionVariable
3
+
4
+ def initialize
5
+ @waiting_threads_sleepers = []
6
+ @mutex = ::Mutex.new
7
+ end
8
+
9
+ def any_waiting_threads?
10
+ waiting_threads_count >= 1
11
+ end
12
+
13
+ def broadcast
14
+ @mutex.synchronize do
15
+ signal_next until @waiting_threads_sleepers.empty?
16
+ end
17
+ self
18
+ end
19
+
20
+ def signal
21
+ @mutex.synchronize do
22
+ signal_next if @waiting_threads_sleepers.any?
23
+ end
24
+ self
25
+ end
26
+
27
+ def wait(mutex = nil, timeout = nil)
28
+ validate_mutex(mutex)
29
+ if timeout != nil && !timeout.is_a?(Numeric)
30
+ raise ArgumentError, "'timeout' argument must be nil or a Numeric"
31
+ end
32
+ sleeper = UninterruptibleSleeper.for_current
33
+ @mutex.synchronize { @waiting_threads_sleepers.push(sleeper) }
34
+ if mutex
35
+ if mutex.respond_to?(:unlock!)
36
+ mutex.unlock! { sleep(sleeper, timeout) }
37
+ else
38
+ mutex.unlock
39
+ sleep(sleeper, timeout)
40
+ mutex.lock
41
+ end
42
+ else
43
+ sleep(sleeper, timeout)
44
+ end
45
+ @mutex.synchronize { @waiting_threads_sleepers.delete(sleeper) }
46
+ self
47
+ end
48
+
49
+ def waiting_threads_count
50
+ @waiting_threads_sleepers.length
51
+ end
52
+
53
+ private
54
+
55
+ # @api private
56
+ def signal_next
57
+ next_waiting_thread_sleeper = @waiting_threads_sleepers.shift
58
+ next_waiting_thread_sleeper.run_thread if next_waiting_thread_sleeper
59
+ nil
60
+ end
61
+
62
+ # @api private
63
+ def sleep(sleeper, duration)
64
+ if duration == nil || duration == Float::INFINITY
65
+ sleeper.stop_thread
66
+ else
67
+ sleeper.sleep_thread(timeout)
68
+ end
69
+ nil
70
+ end
71
+
72
+ def validate_mutex(mutex)
73
+ return if mutex == nil
74
+ return if mutex.respond_to?(:lock) && (mutex.respond_to?(:unlock) || mutex.respond_to?(:unlock!))
75
+ raise ArgumentError, "'mutex' must respond to 'lock' and ('unlock' or'unlock!')"
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,73 @@
1
+ module QuackConcurrency
2
+ class UninterruptibleSleeper
3
+
4
+ def self.for_current
5
+ new(Thread.current)
6
+ end
7
+
8
+ def initialize(thread)
9
+ raise ArgumentError, "'thread' must be a Thread" unless thread.is_a?(Thread)
10
+ @thread = thread
11
+ @state = :running
12
+ @mutex = ::Mutex.new
13
+ @stop_called = false
14
+ @run_called = false
15
+ end
16
+
17
+ def run_thread
18
+ @mutex.synchronize do
19
+ raise '#run_thread has already been called once' if @run_called
20
+ @run_called = true
21
+ return if @state == :running
22
+ Thread.pass until @state = :running || @thread.status == 'sleep'
23
+ @state = :running
24
+ @thread.run
25
+ end
26
+ nil
27
+ end
28
+
29
+ def sleep_thread(duration)
30
+ start_time = Time.now
31
+ stop_thread(timeout: duration)
32
+ time_elapsed = Time.now - start_time
33
+ end
34
+
35
+ def stop_thread(timeout: nil)
36
+ raise 'can only stop current Thread' unless Thread.current == @thread
37
+ raise "'timeout' argument must be nil or a Numeric" if timeout != nil && !timeout.is_a?(Numeric)
38
+ raise '#stop_thread has already been called once' if @stop_called
39
+ @stop_called = true
40
+ target_end_time = Time.now + timeout if timeout
41
+ @mutex.synchronize do
42
+ return if @run_called
43
+ @state = :sleeping
44
+ @mutex.unlock
45
+ loop do
46
+ if timeout
47
+ time_left = target_end_time - Time.now
48
+ Kernel.sleep(time_left) if time_left > 0
49
+ else
50
+ Thread.stop
51
+ end
52
+ break if @state == :running || Time.now >= target_time
53
+ end
54
+ @state = :running
55
+
56
+ # we relock the mutex ensure #run_thread has finshed before #stop_thread
57
+ # if Thread#run is called by another part of the code at the same time as
58
+ # #run_thread is being called, we dont want the call to #run_thread
59
+ # to call Thread#run on a Thread has already resumed and stopped again
60
+ @mutex.lock
61
+ end
62
+ nil
63
+ end
64
+
65
+ private
66
+
67
+ # @api private
68
+ def current?
69
+ Thread.current == @thread
70
+ end
71
+
72
+ end
73
+ end
@@ -1,27 +1,57 @@
1
1
  module QuackConcurrency
2
- class Waiter < ConcurrencyTool
2
+
3
+ # @api private
4
+ class Waiter
3
5
 
4
6
  # Creates a new {Waiter} 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.
7
7
  # @return [Waiter]
8
- def initialize(duck_types: nil)
9
- @queue = Queue.new(duck_types: duck_types)
8
+ def initialize
9
+ @condition_variable = UninterruptibleConditionVariable.new
10
+ @resume_all_forever = false
11
+ @mutex = ::Mutex.new
12
+ end
13
+
14
+ def any_waiting_threads?
15
+ @condition_variable.any_waiting_threads?
16
+ end
17
+
18
+ # Resumes all current and future waiting Thread.
19
+ # @return [void]
20
+ def resume_all
21
+ @condition_variable.broadcast
22
+ nil
10
23
  end
11
24
 
12
- # Resumes next waiting thread.
13
- # @param value value to pass to waiting thread
25
+ # Resumes all current and future waiting Thread.
14
26
  # @return [void]
15
- def resume(value = nil)
16
- @queue << value
27
+ def resume_all_forever
28
+ @mutex.synchronize do
29
+ @resume_all_forever = true
30
+ resume_all
31
+ end
17
32
  nil
18
33
  end
19
34
 
20
- # Waits for another thread to resume the calling thread.
35
+ # Resumes next waiting Thread if one exists.
36
+ # @return [void]
37
+ def resume_one
38
+ @condition_variable.signal
39
+ nil
40
+ end
41
+
42
+ # Waits for another Thread to resume the calling Thread.
21
43
  # @note Will block until resumed.
22
- # @return value passed from resuming thread
44
+ # @return [void]
23
45
  def wait
24
- @queue.pop
46
+ @mutex.synchronize do
47
+ return if @resume_all_forever
48
+ @condition_variable.wait(@mutex)
49
+ end
50
+ nil
51
+ end
52
+
53
+ def waiting_threads_count
54
+ @condition_variable.waiting_threads_count
25
55
  end
26
56
 
27
57
  end
@@ -0,0 +1,35 @@
1
+ # not ready yet
2
+
3
+ module Threadesque
4
+ class SafeYielder
5
+
6
+ def self.for_current
7
+ new(Thread.current)
8
+ end
9
+
10
+ def initialize(thread)
11
+ raise 'not ready yet'
12
+ @thread = thread
13
+ @state = :running
14
+ end
15
+
16
+ def resume
17
+ raise 'Thread is not sleeping' unless @state == :sleeping
18
+ :wait until @thread.status == 'sleep'
19
+ @state = :running
20
+ @thread.run
21
+ nil
22
+ end
23
+
24
+ def yield
25
+ raise 'can only stop current Thread' unless Thread.current == @thread
26
+ @state = :sleeping
27
+ loop do
28
+ Thread.stop
29
+ redo if @state == :sleeping
30
+ end
31
+ nil
32
+ end
33
+
34
+ end
35
+ end
@@ -1,18 +1,19 @@
1
1
  require 'thread'
2
+ require 'reentrant_mutex'
2
3
 
3
- require 'quack_concurrency/concurrency_tool'
4
+ require 'quack_concurrency/condition_variable'
4
5
  require 'quack_concurrency/error'
5
6
  require 'quack_concurrency/future'
6
- require 'quack_concurrency/name'
7
- require 'quack_concurrency/queue'
8
- require 'quack_concurrency/reentrant_mutex'
9
- require 'quack_concurrency/semaphore'
10
- require 'quack_concurrency/waiter'
11
7
  require 'quack_concurrency/future/canceled'
12
8
  require 'quack_concurrency/future/complete'
9
+ require 'quack_concurrency/mutex'
10
+ require 'quack_concurrency/queue'
13
11
  require 'quack_concurrency/queue/error'
12
+ require 'quack_concurrency/reentrant_mutex'
14
13
  require 'quack_concurrency/reentrant_mutex/error'
15
- require 'quack_concurrency/semaphore/error'
14
+ require 'quack_concurrency/uninterruptible_condition_variable'
15
+ require 'quack_concurrency/uninterruptible_sleeper'
16
+ require 'quack_concurrency/waiter'
16
17
 
17
18
 
18
19
  # if you pass a duck type Hash to any of the concurrency tools it will force you to
data/spec/future_spec.rb CHANGED
@@ -2,39 +2,6 @@ require 'quack_concurrency'
2
2
 
3
3
  RSpec.describe QuackConcurrency::Future do
4
4
 
5
- describe "::new" do
6
-
7
- context "when called without a 'duck_types' argument" do
8
- it "should create a new QuackConcurrency::Future" do
9
- future = QuackConcurrency::Future.new
10
- expect(future).to be_a(QuackConcurrency::Future)
11
- end
12
- end
13
-
14
- context "when called with 'condition_variable' and 'mutex' duck types" do
15
- it "should create a new QuackConcurrency::Future" do
16
- duck_types = {condition_variable: Class.new, mutex: Class.new}
17
- future = QuackConcurrency::Future.new(duck_types: duck_types)
18
- expect(future).to be_a(QuackConcurrency::Future)
19
- end
20
- end
21
-
22
- context "when called with only 'condition_variable' duck type" do
23
- it "should raise ArgumentError" do
24
- duck_types = {condition_variable: Class.new}
25
- expect{ QuackConcurrency::Future.new(duck_types: duck_types) }.to raise_error(ArgumentError)
26
- end
27
- end
28
-
29
- context "when called with only 'mutex' duck type" do
30
- it "should raise ArgumentError" do
31
- duck_types = {mutex: Class.new}
32
- expect{ QuackConcurrency::Future.new(duck_types: duck_types) }.to raise_error(ArgumentError)
33
- end
34
- end
35
-
36
- end
37
-
38
5
  describe "#set" do
39
6
 
40
7
  context "when called" do
@@ -97,7 +64,7 @@ RSpec.describe QuackConcurrency::Future do
97
64
  future = QuackConcurrency::Future.new
98
65
  thread = Thread.new do
99
66
  sleep 1
100
- future.set 1
67
+ future.set(1)
101
68
  end
102
69
  start_time = Time.now
103
70
  expect(future.get).to eql 1
data/spec/queue_spec.rb CHANGED
@@ -2,39 +2,6 @@ require 'quack_concurrency'
2
2
 
3
3
  RSpec.describe QuackConcurrency::Queue do
4
4
 
5
- describe "::new" do
6
-
7
- context "when called without a 'duck_types' argument" do
8
- it "should create a new QuackConcurrency::Queue" do
9
- queue = QuackConcurrency::Queue.new
10
- expect(queue).to be_a(QuackConcurrency::Queue)
11
- end
12
- end
13
-
14
- context "when called with 'condition_variable' and 'mutex' duck types" do
15
- it "should create a new QuackConcurrency::Queue" do
16
- duck_types = {condition_variable: Class.new, mutex: Class.new}
17
- queue = QuackConcurrency::Queue.new(duck_types: duck_types)
18
- expect(queue).to be_a(QuackConcurrency::Queue)
19
- end
20
- end
21
-
22
- context "when called with only 'condition_variable' duck type" do
23
- it "should raise ArgumentError" do
24
- duck_types = {condition_variable: Class.new}
25
- expect{ QuackConcurrency::Queue.new(duck_types: duck_types) }.to raise_error(ArgumentError)
26
- end
27
- end
28
-
29
- context "when called with only 'mutex' duck type" do
30
- it "should raise ArgumentError" do
31
- duck_types = {mutex: Class.new}
32
- expect{ QuackConcurrency::Queue.new(duck_types: duck_types) }.to raise_error(ArgumentError)
33
- end
34
- end
35
-
36
- end
37
-
38
5
  describe "#push" do
39
6
 
40
7
  context "when called many times when queue is not closed" do
@@ -52,7 +19,7 @@ RSpec.describe QuackConcurrency::Queue do
52
19
  context "when #pop is called with non_block set to true" do
53
20
  it "should raise Error" do
54
21
  queue = QuackConcurrency::Queue.new
55
- expect{ queue.pop(true) }.to raise_error(QuackConcurrency::Queue::Error)
22
+ expect{ queue.pop(true) }.to raise_error(ThreadError)
56
23
  end
57
24
  end
58
25
 
@@ -2,39 +2,6 @@ require 'quack_concurrency'
2
2
 
3
3
  RSpec.describe QuackConcurrency::ReentrantMutex do
4
4
 
5
- describe "::new" do
6
-
7
- context "when called without a 'duck_types' argument" do
8
- it "should create a new QuackConcurrency::ReentrantMutex" do
9
- mutex = QuackConcurrency::ReentrantMutex.new
10
- expect(mutex).to be_a(QuackConcurrency::ReentrantMutex)
11
- end
12
- end
13
-
14
- context "when called with 'condition_variable' and 'mutex' duck types" do
15
- it "should create a new QuackConcurrency::ReentrantMutex" do
16
- duck_types = {condition_variable: Class.new, mutex: Class.new}
17
- mutex = QuackConcurrency::ReentrantMutex.new(duck_types: duck_types)
18
- expect(mutex).to be_a(QuackConcurrency::ReentrantMutex)
19
- end
20
- end
21
-
22
- context "when called with only 'condition_variable' duck type" do
23
- it "should raise ArgumentError" do
24
- duck_types = {condition_variable: Class.new}
25
- expect{ QuackConcurrency::ReentrantMutex.new(duck_types: duck_types) }.to raise_error(ArgumentError)
26
- end
27
- end
28
-
29
- context "when called with only 'mutex' duck type" do
30
- it "should raise ArgumentError" do
31
- duck_types = {mutex: Class.new}
32
- expect{ QuackConcurrency::ReentrantMutex.new(duck_types: duck_types) }.to raise_error(ArgumentError)
33
- end
34
- end
35
-
36
- end
37
-
38
5
  describe "#lock" do
39
6
 
40
7
  context "when called for first time" do
@@ -164,6 +131,8 @@ RSpec.describe QuackConcurrency::ReentrantMutex do
164
131
  it "should wait for that time" do
165
132
  mutex = QuackConcurrency::ReentrantMutex.new
166
133
  start_time = Time.now
134
+ #require 'pry'
135
+ #binding.pry
167
136
  mutex.synchronize do
168
137
  mutex.sleep(1)
169
138
  end