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