Skip to content
1 change: 1 addition & 0 deletions lib/concurrent-edge.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

require 'concurrent/edge/future'
require 'concurrent/edge/lock_free_stack'
require 'concurrent/edge/atomic_markable_reference'
6 changes: 3 additions & 3 deletions lib/concurrent.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
require 'concurrent/tvar'

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

# Modern concurrency tools for Ruby. Inspired by Erlang, Clojure, Scala, Haskell,
# F#, C#, Java, and classic concurrency patterns.
#
#
# The design goals of this gem are:
#
#
# * Stay true to the spirit of the languages providing inspiration
# * But implement in a way that makes sense for Ruby
# * Keep the semantics as idiomatic Ruby as possible
Expand Down
175 changes: 175 additions & 0 deletions lib/concurrent/edge/atomic_markable_reference.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
module Concurrent
module Edge
# @!macro atomic_markable_reference
class AtomicMarkableReference < ::Concurrent::Synchronization::Object
# @!macro [attach] atomic_markable_reference_method_initialize
def initialize(value = nil, mark = false)
super()
@Reference = AtomicReference.new ImmutableArray[value, mark]
ensure_ivar_visibility!
end

# @!macro [attach] atomic_markable_reference_method_compare_and_set
#
# Atomically sets the value and mark to the given updated value and
# mark given both:
# - the current value == the expected value &&
# - the current mark == the expected mark
#
# @param [Object] old_val the expected value
# @param [Object] new_val the new value
# @param [Boolean] old_mark the expected mark
# @param [Boolean] new_mark the new mark
#
# @return [Boolean] `true` if successful. A `false` return indicates
# that the actual value was not equal to the expected value or the
# actual mark was not equal to the expected mark
def compare_and_set(expected_val, new_val, expected_mark, new_mark)
# Memoize a valid reference to the current AtomicReference for
# later comparison.
current = @Reference.get
curr_val, curr_mark = current

# Ensure that that the expected marks match.
return false unless expected_mark == curr_mark

if expected_val.is_a? Numeric
# If the object is a numeric, we need to ensure we are comparing
# the numerical values
return false unless expected_val == curr_val
else
# Otherwise, we need to ensure we are comparing the object identity.
# Theoretically, this could be incorrect if a user monkey-patched
# `Object#equal?`, but they should know that they are playing with
# fire at that point.
return false unless expected_val.equal? curr_val
end

prospect = ImmutableArray[new_val, new_mark]

@Reference.compare_and_set current, prospect
end
alias_method :compare_and_swap, :compare_and_set

# @!macro [attach] atomic_markable_reference_method_get
#
# Gets the current reference and marked values.
#
# @return [ImmutableArray] the current reference and marked values
def get
@Reference.get
end

# @!macro [attach] atomic_markable_reference_method_value
#
# Gets the current value of the reference
#
# @return [Object] the current value of the reference
def value
@Reference.get[0]
end

# @!macro [attach] atomic_markable_reference_method_mark
#
# Gets the current marked value
#
# @return [Boolean] the current marked value
def mark
@Reference.get[1]
end
alias_method :marked?, :mark

# @!macro [attach] atomic_markable_reference_method_set
#
# _Unconditionally_ sets to the given value of both the reference and
# the mark.
#
# @param [Object] new_val the new value
# @param [Boolean] new_mark the new mark
#
# @return [ImmutableArray] both the new value and the new mark
def set(new_val, new_mark)
@Reference.set ImmutableArray[new_val, new_mark]
end

# @!macro [attach] atomic_markable_reference_method_update
#
# Pass the current value and marked state to the given block, replacing it
# with the block's results. May retry if the value changes during the
# block's execution.
#
# @yield [Object] Calculate a new value and marked state for the atomic
# reference using given (old) value and (old) marked
# @yieldparam [Object] old_val the starting value of the atomic reference
# @yieldparam [Boolean] old_mark the starting state of marked
#
# @return [ImmutableArray] the new value and new mark
def update
loop do
old_val, old_mark = @Reference.get
new_val, new_mark = yield old_val, old_mark

if compare_and_set old_val, new_val, old_mark, new_mark
return ImmutableArray[new_val, new_mark]
end
end
end

# @!macro [attach] atomic_markable_reference_method_try_update!
#
# Pass the current value to the given block, replacing it
# with the block's result. Raise an exception if the update
# fails.
#
# @yield [Object] Calculate a new value and marked state for the atomic
# reference using given (old) value and (old) marked
# @yieldparam [Object] old_val the starting value of the atomic reference
# @yieldparam [Boolean] old_mark the starting state of marked
#
# @return [ImmutableArray] the new value and marked state
#
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
def try_update!
old_val, old_mark = @Reference.get
new_val, new_mark = yield old_val, old_mark

unless compare_and_set old_val, new_val, old_mark, new_mark
fail ::Concurrent::ConcurrentUpdateError,
'AtomicMarkableReference: Update failed due to race condition.',
'Note: If you would like to guarantee an update, please use ' \
'the `AtomicMarkableReference#update` method.'
end

ImmutableArray[new_val, new_mark]
end

# @!macro [attach] atomic_markable_reference_method_try_update
#
# Pass the current value to the given block, replacing it with the
# block's result. Simply return nil if update fails.
#
# @yield [Object] Calculate a new value and marked state for the atomic
# reference using given (old) value and (old) marked
# @yieldparam [Object] old_val the starting value of the atomic reference
# @yieldparam [Boolean] old_mark the starting state of marked
#
# @return [ImmutableArray] the new value and marked state, or nil if
# the update failed
def try_update
old_val, old_mark = @Reference.get
new_val, new_mark = yield old_val, old_mark

return unless compare_and_set old_val, new_val, old_mark, new_mark

ImmutableArray[new_val, new_mark]
end

# Internal/private ImmutableArray for representing pairs
class ImmutableArray < Array
def self.new(*args)
super(*args).freeze
end
end
end
end
end
152 changes: 152 additions & 0 deletions spec/concurrent/atomic/atomic_markable_reference_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
describe Concurrent::Edge::AtomicMarkableReference do
subject { described_class.new 1000, true }

describe '.initialize' do
it 'constructs the object' do
expect(subject.value).to eq 1000
expect(subject.marked?).to eq true
end

it 'has sane defaults' do
amr = described_class.new

expect(amr.value).to eq nil
expect(amr.marked?).to eq false
end
end

describe '#set' do
it 'sets the value and mark' do
val, mark = subject.set 1001, true

expect(subject.value).to eq 1001
expect(subject.marked?).to eq true
expect(val).to eq 1001
expect(mark).to eq true
end
end

describe '#try_update!' do
it 'updates the value and mark' do
val, mark = subject.try_update! { |v, m| [v + 1, !m] }

expect(subject.value).to eq 1001
expect(val).to eq 1001
expect(mark).to eq false
end

it 'raises ConcurrentUpdateError when attempting to set inside of block' do
expect do
subject.try_update! do |v, m|
subject.set(1001, false)
[v + 1, !m]
end
end.to raise_error Concurrent::ConcurrentUpdateError
end
end

describe '#try_update' do
it 'updates the value and mark' do
val, mark = subject.try_update { |v, m| [v + 1, !m] }

expect(subject.value).to eq 1001
expect(val).to eq 1001
expect(mark).to eq false
end

it 'returns nil when attempting to set inside of block' do
expect do
subject.try_update do |v, m|
subject.set(1001, false)
[v + 1, !m]
end.to eq nil
end
end
end

describe '#update' do
it 'updates the value and mark' do
val, mark = subject.update { |v, m| [v + 1, !m] }

expect(subject.value).to eq 1001
expect(subject.marked?).to eq false

expect(val).to eq 1001
expect(mark).to eq false
end

it 'retries until update succeeds' do
tries = 0

subject.update do |v, m|
tries += 1
subject.set(1001, false)
[v + 1, !m]
end

expect(tries).to eq 2
end
end

describe '#compare_and_set' do
context 'when objects have the same identity' do
it 'sets the value and mark' do
arr = [1, 2, 3]
subject.set(arr, true)
expect(subject.compare_and_set(arr, 1.2, true, false)).to be_truthy
end
end

context 'when objects have the different identity' do
it 'it does not set the value or mark' do
subject.set([1, 2, 3], true)
expect(subject.compare_and_set([1, 2, 3], 1.2, true, false))
.to be_falsey
end

context 'when comparing Numeric objects' do
context 'Non-idepotent Float' do
it 'sets the value and mark' do
subject.set(1.0 + 0.1, true)
expect(subject.compare_and_set(1.0 + 0.1, 1.2, true, false))
.to be_truthy
end
end

context 'BigNum' do
it 'sets the value and mark' do
subject.set(2**100, false)
expect(subject.compare_and_set(2**100, 2**99, false, true))
.to be_truthy
end
end

context 'Rational' do
it 'sets the value and mark' do
require 'rational' unless ''.respond_to? :to_r
subject.set(Rational(1, 3), true)
comp = subject.compare_and_set(Rational(1, 3),
Rational(3, 1),
true,
false)
expect(comp).to be_truthy
end
end
end

context 'Rational' do
it 'is successful' do
# Complex
require 'complex' unless ''.respond_to? :to_c
subject.set(Complex(1, 2), false)
comp = subject.compare_and_set(Complex(1, 2),
Complex(1, 3),
false,
true)
expect(comp)
.to be_truthy
end
end
end
end
end