Skip to content

AtomicMarkableReference: Add pure Ruby implementation #281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 7, 2015
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