Skip to content

Commit f2aaa92

Browse files
committed
Merge pull request #281 from ianks/atomic-markable-reference
AtomicMarkableReference: Add pure Ruby implementation
2 parents 24336f5 + 6f663b3 commit f2aaa92

File tree

4 files changed

+331
-3
lines changed

4 files changed

+331
-3
lines changed

lib/concurrent-edge.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88

99
require 'concurrent/edge/future'
1010
require 'concurrent/edge/lock_free_stack'
11+
require 'concurrent/edge/atomic_markable_reference'

lib/concurrent.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
require 'concurrent/tvar'
2828

2929
# @!macro [new] monotonic_clock_warning
30-
#
30+
#
3131
# @note Time calculations one all platforms and languages are sensitive to
3232
# changes to the system clock. To alleviate the potential problems
3333
# associated with changing the system clock while an application is running,
@@ -46,9 +46,9 @@
4646

4747
# Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell,
4848
# F#, C#, Java, and classic concurrency patterns.
49-
#
49+
#
5050
# The design goals of this gem are:
51-
#
51+
#
5252
# * Stay true to the spirit of the languages providing inspiration
5353
# * But implement in a way that makes sense for Ruby
5454
# * Keep the semantics as idiomatic Ruby as possible
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
module Concurrent
2+
module Edge
3+
# @!macro atomic_markable_reference
4+
class AtomicMarkableReference < ::Concurrent::Synchronization::Object
5+
# @!macro [attach] atomic_markable_reference_method_initialize
6+
def initialize(value = nil, mark = false)
7+
super()
8+
@Reference = AtomicReference.new ImmutableArray[value, mark]
9+
ensure_ivar_visibility!
10+
end
11+
12+
# @!macro [attach] atomic_markable_reference_method_compare_and_set
13+
#
14+
# Atomically sets the value and mark to the given updated value and
15+
# mark given both:
16+
# - the current value == the expected value &&
17+
# - the current mark == the expected mark
18+
#
19+
# @param [Object] old_val the expected value
20+
# @param [Object] new_val the new value
21+
# @param [Boolean] old_mark the expected mark
22+
# @param [Boolean] new_mark the new mark
23+
#
24+
# @return [Boolean] `true` if successful. A `false` return indicates
25+
# that the actual value was not equal to the expected value or the
26+
# actual mark was not equal to the expected mark
27+
def compare_and_set(expected_val, new_val, expected_mark, new_mark)
28+
# Memoize a valid reference to the current AtomicReference for
29+
# later comparison.
30+
current = @Reference.get
31+
curr_val, curr_mark = current
32+
33+
# Ensure that that the expected marks match.
34+
return false unless expected_mark == curr_mark
35+
36+
if expected_val.is_a? Numeric
37+
# If the object is a numeric, we need to ensure we are comparing
38+
# the numerical values
39+
return false unless expected_val == curr_val
40+
else
41+
# Otherwise, we need to ensure we are comparing the object identity.
42+
# Theoretically, this could be incorrect if a user monkey-patched
43+
# `Object#equal?`, but they should know that they are playing with
44+
# fire at that point.
45+
return false unless expected_val.equal? curr_val
46+
end
47+
48+
prospect = ImmutableArray[new_val, new_mark]
49+
50+
@Reference.compare_and_set current, prospect
51+
end
52+
alias_method :compare_and_swap, :compare_and_set
53+
54+
# @!macro [attach] atomic_markable_reference_method_get
55+
#
56+
# Gets the current reference and marked values.
57+
#
58+
# @return [ImmutableArray] the current reference and marked values
59+
def get
60+
@Reference.get
61+
end
62+
63+
# @!macro [attach] atomic_markable_reference_method_value
64+
#
65+
# Gets the current value of the reference
66+
#
67+
# @return [Object] the current value of the reference
68+
def value
69+
@Reference.get[0]
70+
end
71+
72+
# @!macro [attach] atomic_markable_reference_method_mark
73+
#
74+
# Gets the current marked value
75+
#
76+
# @return [Boolean] the current marked value
77+
def mark
78+
@Reference.get[1]
79+
end
80+
alias_method :marked?, :mark
81+
82+
# @!macro [attach] atomic_markable_reference_method_set
83+
#
84+
# _Unconditionally_ sets to the given value of both the reference and
85+
# the mark.
86+
#
87+
# @param [Object] new_val the new value
88+
# @param [Boolean] new_mark the new mark
89+
#
90+
# @return [ImmutableArray] both the new value and the new mark
91+
def set(new_val, new_mark)
92+
@Reference.set ImmutableArray[new_val, new_mark]
93+
end
94+
95+
# @!macro [attach] atomic_markable_reference_method_update
96+
#
97+
# Pass the current value and marked state to the given block, replacing it
98+
# with the block's results. May retry if the value changes during the
99+
# block's execution.
100+
#
101+
# @yield [Object] Calculate a new value and marked state for the atomic
102+
# reference using given (old) value and (old) marked
103+
# @yieldparam [Object] old_val the starting value of the atomic reference
104+
# @yieldparam [Boolean] old_mark the starting state of marked
105+
#
106+
# @return [ImmutableArray] the new value and new mark
107+
def update
108+
loop do
109+
old_val, old_mark = @Reference.get
110+
new_val, new_mark = yield old_val, old_mark
111+
112+
if compare_and_set old_val, new_val, old_mark, new_mark
113+
return ImmutableArray[new_val, new_mark]
114+
end
115+
end
116+
end
117+
118+
# @!macro [attach] atomic_markable_reference_method_try_update!
119+
#
120+
# Pass the current value to the given block, replacing it
121+
# with the block's result. Raise an exception if the update
122+
# fails.
123+
#
124+
# @yield [Object] Calculate a new value and marked state for the atomic
125+
# reference using given (old) value and (old) marked
126+
# @yieldparam [Object] old_val the starting value of the atomic reference
127+
# @yieldparam [Boolean] old_mark the starting state of marked
128+
#
129+
# @return [ImmutableArray] the new value and marked state
130+
#
131+
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
132+
def try_update!
133+
old_val, old_mark = @Reference.get
134+
new_val, new_mark = yield old_val, old_mark
135+
136+
unless compare_and_set old_val, new_val, old_mark, new_mark
137+
fail ::Concurrent::ConcurrentUpdateError,
138+
'AtomicMarkableReference: Update failed due to race condition.',
139+
'Note: If you would like to guarantee an update, please use ' \
140+
'the `AtomicMarkableReference#update` method.'
141+
end
142+
143+
ImmutableArray[new_val, new_mark]
144+
end
145+
146+
# @!macro [attach] atomic_markable_reference_method_try_update
147+
#
148+
# Pass the current value to the given block, replacing it with the
149+
# block's result. Simply return nil if update fails.
150+
#
151+
# @yield [Object] Calculate a new value and marked state for the atomic
152+
# reference using given (old) value and (old) marked
153+
# @yieldparam [Object] old_val the starting value of the atomic reference
154+
# @yieldparam [Boolean] old_mark the starting state of marked
155+
#
156+
# @return [ImmutableArray] the new value and marked state, or nil if
157+
# the update failed
158+
def try_update
159+
old_val, old_mark = @Reference.get
160+
new_val, new_mark = yield old_val, old_mark
161+
162+
return unless compare_and_set old_val, new_val, old_mark, new_mark
163+
164+
ImmutableArray[new_val, new_mark]
165+
end
166+
167+
# Internal/private ImmutableArray for representing pairs
168+
class ImmutableArray < Array
169+
def self.new(*args)
170+
super(*args).freeze
171+
end
172+
end
173+
end
174+
end
175+
end
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
describe Concurrent::Edge::AtomicMarkableReference do
2+
subject { described_class.new 1000, true }
3+
4+
describe '.initialize' do
5+
it 'constructs the object' do
6+
expect(subject.value).to eq 1000
7+
expect(subject.marked?).to eq true
8+
end
9+
10+
it 'has sane defaults' do
11+
amr = described_class.new
12+
13+
expect(amr.value).to eq nil
14+
expect(amr.marked?).to eq false
15+
end
16+
end
17+
18+
describe '#set' do
19+
it 'sets the value and mark' do
20+
val, mark = subject.set 1001, true
21+
22+
expect(subject.value).to eq 1001
23+
expect(subject.marked?).to eq true
24+
expect(val).to eq 1001
25+
expect(mark).to eq true
26+
end
27+
end
28+
29+
describe '#try_update!' do
30+
it 'updates the value and mark' do
31+
val, mark = subject.try_update! { |v, m| [v + 1, !m] }
32+
33+
expect(subject.value).to eq 1001
34+
expect(val).to eq 1001
35+
expect(mark).to eq false
36+
end
37+
38+
it 'raises ConcurrentUpdateError when attempting to set inside of block' do
39+
expect do
40+
subject.try_update! do |v, m|
41+
subject.set(1001, false)
42+
[v + 1, !m]
43+
end
44+
end.to raise_error Concurrent::ConcurrentUpdateError
45+
end
46+
end
47+
48+
describe '#try_update' do
49+
it 'updates the value and mark' do
50+
val, mark = subject.try_update { |v, m| [v + 1, !m] }
51+
52+
expect(subject.value).to eq 1001
53+
expect(val).to eq 1001
54+
expect(mark).to eq false
55+
end
56+
57+
it 'returns nil when attempting to set inside of block' do
58+
expect do
59+
subject.try_update do |v, m|
60+
subject.set(1001, false)
61+
[v + 1, !m]
62+
end.to eq nil
63+
end
64+
end
65+
end
66+
67+
describe '#update' do
68+
it 'updates the value and mark' do
69+
val, mark = subject.update { |v, m| [v + 1, !m] }
70+
71+
expect(subject.value).to eq 1001
72+
expect(subject.marked?).to eq false
73+
74+
expect(val).to eq 1001
75+
expect(mark).to eq false
76+
end
77+
78+
it 'retries until update succeeds' do
79+
tries = 0
80+
81+
subject.update do |v, m|
82+
tries += 1
83+
subject.set(1001, false)
84+
[v + 1, !m]
85+
end
86+
87+
expect(tries).to eq 2
88+
end
89+
end
90+
91+
describe '#compare_and_set' do
92+
context 'when objects have the same identity' do
93+
it 'sets the value and mark' do
94+
arr = [1, 2, 3]
95+
subject.set(arr, true)
96+
expect(subject.compare_and_set(arr, 1.2, true, false)).to be_truthy
97+
end
98+
end
99+
100+
context 'when objects have the different identity' do
101+
it 'it does not set the value or mark' do
102+
subject.set([1, 2, 3], true)
103+
expect(subject.compare_and_set([1, 2, 3], 1.2, true, false))
104+
.to be_falsey
105+
end
106+
107+
context 'when comparing Numeric objects' do
108+
context 'Non-idepotent Float' do
109+
it 'sets the value and mark' do
110+
subject.set(1.0 + 0.1, true)
111+
expect(subject.compare_and_set(1.0 + 0.1, 1.2, true, false))
112+
.to be_truthy
113+
end
114+
end
115+
116+
context 'BigNum' do
117+
it 'sets the value and mark' do
118+
subject.set(2**100, false)
119+
expect(subject.compare_and_set(2**100, 2**99, false, true))
120+
.to be_truthy
121+
end
122+
end
123+
124+
context 'Rational' do
125+
it 'sets the value and mark' do
126+
require 'rational' unless ''.respond_to? :to_r
127+
subject.set(Rational(1, 3), true)
128+
comp = subject.compare_and_set(Rational(1, 3),
129+
Rational(3, 1),
130+
true,
131+
false)
132+
expect(comp).to be_truthy
133+
end
134+
end
135+
end
136+
137+
context 'Rational' do
138+
it 'is successful' do
139+
# Complex
140+
require 'complex' unless ''.respond_to? :to_c
141+
subject.set(Complex(1, 2), false)
142+
comp = subject.compare_and_set(Complex(1, 2),
143+
Complex(1, 3),
144+
false,
145+
true)
146+
expect(comp)
147+
.to be_truthy
148+
end
149+
end
150+
end
151+
end
152+
end

0 commit comments

Comments
 (0)