Skip to content

Commit ffb2ffb

Browse files
committed
AtomicMarkableReference: Add initial Ruby version
This commit adds a pure Ruby implementation of an AtomicMarkableReference. An AtomicMarkableReference is much like an AtomicReference with that added property that it can be, you guessed it, marked either true or false. They are useful for implementing more advanced data sctructures such as recursive split-ordered hash tables, etc.
1 parent dcac23a commit ffb2ffb

File tree

3 files changed

+313
-0
lines changed

3 files changed

+313
-0
lines changed

lib/concurrent.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
require 'concurrent/executors'
1010
require 'concurrent/utilities'
1111

12+
require 'concurrent/atomic/atomic_markable_reference'
1213
require 'concurrent/atomic/atomic_reference'
1314
require 'concurrent/atom'
1415
require 'concurrent/async'
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
module Edge
2+
module Concurrent
3+
# @!macro atomic_markable_reference
4+
class AtomicMarkableReference < ::Concurrent::Synchronization::Object
5+
# TODO: Remove once out of edge module
6+
include ::Concurrent
7+
8+
# @!macro [attach] atomic_markable_reference_method_initialize
9+
def initialize(value = nil, mark = false)
10+
super
11+
@reference = AtomicReference.new ImmutableArray[value, mark]
12+
ensure_ivar_visibility!
13+
end
14+
15+
# @!macro [attach] atomic_markable_reference_method_compare_and_set
16+
#
17+
# Atomically sets the value and mark to the given updated value and
18+
# mark given both:
19+
# - the current value == the expected value &&
20+
# - the current mark == the expected mark
21+
#
22+
# @param [Object] old_val the expected value
23+
# @param [Object] new_val the new value
24+
# @param [Boolean] old_mark the expected mark
25+
# @param [Boolean] new_mark the new mark
26+
#
27+
# @return [Boolean] `true` if successful. A `false` return indicates
28+
# that the actual value was not equal to the expected value or the
29+
# actual mark was not equal to the expected mark
30+
def compare_and_set(expected_val, new_val, expected_mark, new_mark)
31+
# Memoize a valid reference to the current AtomicReference for
32+
# later comparison.
33+
current = @reference.get
34+
curr_val, curr_mark = current
35+
36+
# Ensure that that the expected values match.
37+
return false unless (expected_val == curr_val) &&
38+
(expected_mark == curr_mark)
39+
40+
# In this case, it would be redundant to set the fields. Just
41+
# shortcircuit without wasting CPU time on CAS.
42+
return true if (new_val == curr_val) &&
43+
(new_mark == curr_mark)
44+
45+
prospect = ImmutableArray[new_val, new_mark]
46+
47+
@reference.compare_and_set current, prospect
48+
end
49+
50+
# @!macro [attach] atomic_markable_reference_method_get
51+
#
52+
# Gets the current reference and marked values.
53+
#
54+
# @return [ImmutableArray] the current reference and marked values
55+
def get
56+
@reference.get
57+
end
58+
59+
# @!macro [attach] atomic_markable_reference_method_value
60+
#
61+
# Gets the current value of the reference
62+
#
63+
# @return [Object] the current value of the reference
64+
def value
65+
@reference.get[0]
66+
end
67+
68+
# @!macro [attach] atomic_markable_reference_method_mark
69+
#
70+
# Gets the current marked value
71+
#
72+
# @return [Boolean] the current marked value
73+
def mark
74+
@reference.get[1]
75+
end
76+
alias_method :marked?, :mark
77+
78+
# @!macro [attach] atomic_markable_reference_method_set
79+
#
80+
# _Unconditionally_ sets to the given value of both the reference and
81+
# the mark.
82+
#
83+
# @param [Object] new_val the new value
84+
# @param [Boolean] new_mark the new mark
85+
#
86+
# @return [ImmutableArray] both the new value and the new mark
87+
def set(new_val, new_mark)
88+
ImmutableArray[new_val, new_mark].tap do |pair|
89+
@reference.set pair
90+
end
91+
end
92+
93+
# @!macro [attach] atomic_markable_reference_method_update
94+
#
95+
# Pass the current value and marked state to the given block, replacing it
96+
# with the block's results. May retry if the value changes during the
97+
# block's execution.
98+
#
99+
# @yield [Object] Calculate a new value and marked state for the atomic
100+
# reference using given (old) value and (old) marked
101+
# @yieldparam [Object] old_val the starting value of the atomic reference
102+
# @yieldparam [Boolean] old_mark the starting state of marked
103+
#
104+
# @return [ImmutableArray] the new value and new mark
105+
def update
106+
loop do
107+
old_val, old_mark = value, marked?
108+
new_val, new_mark = yield old_val, old_mark
109+
110+
if compare_and_set old_val, new_val, old_mark, new_mark
111+
return ImmutableArray[new_val, new_mark]
112+
end
113+
end
114+
end
115+
116+
# @!macro [attach] atomic_markable_reference_method_try_update
117+
#
118+
# Pass the current value to the given block, replacing it
119+
# with the block's result. Raise an exception if the update
120+
# fails.
121+
#
122+
# @yield [Object] Calculate a new value and marked state for the atomic
123+
# reference using given (old) value and (old) marked
124+
# @yieldparam [Object] old_val the starting value of the atomic reference
125+
# @yieldparam [Boolean] old_mark the starting state of marked
126+
#
127+
# @return [ImmutableArray] the new value and marked state
128+
#
129+
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
130+
def try_update
131+
old_val, old_mark = value, marked?
132+
new_val, new_mark = yield old_val, old_mark
133+
134+
unless compare_and_set old_val, new_val, old_mark, new_mark
135+
fail ::Concurrent::ConcurrentUpdateError,
136+
'AtomicMarkableReference: Update failed due to race condition.',
137+
'Note: If you would like to guarantee an update, please use ' \
138+
'the `AtomicMarkableReference#update` method.'
139+
end
140+
141+
ImmutableArray[new_val, new_mark]
142+
end
143+
144+
# Internal/private ImmutableArray for representing pairs
145+
class ImmutableArray < Array
146+
def self.new(*args)
147+
super(*args).freeze
148+
end
149+
end
150+
end
151+
end
152+
end
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
# Concurrent::AtomicMarkableReference;w
2+
shared_examples :atomic_markable_reference do
3+
# use a number outside JRuby's fixnum cache range, to ensure identity is
4+
# preserved
5+
let(:atomic) { Edge::Concurrent::AtomicMarkableReference.new 1000, true }
6+
7+
specify :test_construct do
8+
expect(atomic.value).to eq 1000
9+
expect(atomic.marked?).to eq true
10+
end
11+
12+
specify :test_set do
13+
val, mark = atomic.set(1001, true)
14+
15+
expect(atomic.value).to eq 1001
16+
expect(atomic.marked?).to eq true
17+
18+
expect(val).to eq 1001
19+
expect(mark).to eq true
20+
end
21+
22+
specify :test_update do
23+
val, mark = atomic.update { |v, m| [v + 1, !m] }
24+
25+
expect(atomic.value).to eq 1001
26+
expect(atomic.marked?).to eq false
27+
28+
expect(val).to eq 1001
29+
expect(mark).to eq false
30+
end
31+
32+
specify :test_try_update do
33+
val, mark = atomic.try_update { |v, m| [v + 1, !m] }
34+
35+
expect(atomic.value).to eq 1001
36+
37+
expect(val).to eq 1001
38+
expect(mark).to eq false
39+
end
40+
41+
specify :test_try_update_fails do
42+
expect do
43+
# assigning within block exploits implementation detail for test
44+
atomic.try_update do |v, m|
45+
atomic.set(1001, false)
46+
[v + 1, !m]
47+
end
48+
end.to raise_error Concurrent::ConcurrentUpdateError
49+
end
50+
51+
specify :test_update_retries do
52+
tries = 0
53+
54+
# assigning within block exploits implementation detail for test
55+
atomic.update do |v, m|
56+
tries += 1
57+
atomic.set(1001, false)
58+
[v + 1, !m]
59+
end
60+
61+
expect(tries).to eq 2
62+
end
63+
64+
specify :test_numeric_cas do
65+
# non-idempotent Float (JRuby, Rubinius, MRI < 2.0.0 or 32-bit)
66+
atomic.set(1.0 + 0.1, true)
67+
expect(atomic.compare_and_set(1.0 + 0.1, 1.2, true, false))
68+
.to be_truthy, "CAS failed for (#{1.0 + 0.1}, true) => (1.2, false)"
69+
70+
# Bignum
71+
atomic.set(2**100, false)
72+
expect(atomic.compare_and_set(2**100, 2**99, false, true))
73+
.to be_truthy, "CAS failed for (#{2**100}, false) => (0, true)"
74+
75+
# Rational
76+
require 'rational' unless ''.respond_to? :to_r
77+
atomic.set(Rational(1, 3), true)
78+
expect(atomic.compare_and_set(Rational(1, 3), Rational(3, 1), true, false))
79+
.to be_truthy, "CAS failed for (#{Rational(1, 3)}, true) => (0, false)"
80+
81+
# Complex
82+
require 'complex' unless ''.respond_to? :to_c
83+
atomic.set(Complex(1, 2), false)
84+
expect(atomic.compare_and_set(Complex(1, 2), Complex(1, 3), false, true))
85+
.to be_truthy, "CAS failed for (#{Complex(1, 2)}, false) => (0, false)"
86+
end
87+
end
88+
89+
# Specs for platform specific implementations
90+
module Edge
91+
module Concurrent
92+
describe AtomicMarkableReference do
93+
it_should_behave_like :atomic_markable_reference
94+
end
95+
96+
if defined? Concurrent::CAtomicMarkableReference
97+
describe CAtomicMarkableReference do
98+
skip 'Unimplemented'
99+
end
100+
elsif defined? Concurrent::JavaAtomicMarkableReference
101+
describe JavaAtomicMarkableReference do
102+
skip 'Unimplemented'
103+
end
104+
elsif defined? Concurrent::RbxAtomicMarkableReference
105+
describe RbxAtomicMarkableReference do
106+
skip 'Unimplemented'
107+
end
108+
end
109+
110+
describe AtomicMarkableReference do
111+
if ::Concurrent.on_jruby?
112+
it 'inherits from JavaAtomicMarkableReference' do
113+
skip 'Unimplemented'
114+
end
115+
elsif ::Concurrent.allow_c_extensions?
116+
it 'inherits from CAtomicMarkableReference' do
117+
skip 'Unimplemented'
118+
end
119+
elsif ::Concurrent.on_rbx?
120+
it 'inherits from RbxAtomicMarkableReference' do
121+
skip 'Unimplemented'
122+
end
123+
else
124+
it 'inherits from MutexAtomicMarkableReference' do
125+
skip 'Unimplemented'
126+
end
127+
end
128+
end
129+
130+
if defined? Concurrent::CAtomicMarkableReference
131+
describe CAtomicMarkableReference do
132+
skip 'Unimplemented'
133+
end
134+
elsif defined? Concurrent::JavaAtomicMarkableReference
135+
describe JavaAtomicMarkableReference do
136+
skip 'Unimplemented'
137+
end
138+
elsif defined? Concurrent::RbxAtomicMarkableReference
139+
describe RbxAtomicMarkableReference do
140+
skip 'Unimplemented'
141+
end
142+
end
143+
144+
describe AtomicMarkableReference do
145+
if ::Concurrent.on_jruby?
146+
it 'inherits from JavaAtomicMarkableReference' do
147+
skip 'Unimplemented'
148+
end
149+
elsif ::Concurrent.allow_c_extensions?
150+
it 'inherits from CAtomicMarkableReference' do
151+
skip 'Unimplemented'
152+
end
153+
elsif ::Concurrent.on_rbx?
154+
it 'inherits from RbxAtomicMarkableReference' do
155+
skip 'Unimplemented'
156+
end
157+
end
158+
end
159+
end
160+
end

0 commit comments

Comments
 (0)