diff --git a/lib/concurrent-edge.rb b/lib/concurrent-edge.rb index 2fad35da3..ca3d838f6 100644 --- a/lib/concurrent-edge.rb +++ b/lib/concurrent-edge.rb @@ -8,3 +8,4 @@ require 'concurrent/edge/future' require 'concurrent/edge/lock_free_stack' +require 'concurrent/edge/atomic_markable_reference' diff --git a/lib/concurrent.rb b/lib/concurrent.rb index c5386e37d..7699c578c 100644 --- a/lib/concurrent.rb +++ b/lib/concurrent.rb @@ -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, @@ -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 diff --git a/lib/concurrent/edge/atomic_markable_reference.rb b/lib/concurrent/edge/atomic_markable_reference.rb new file mode 100644 index 000000000..6ccfc0846 --- /dev/null +++ b/lib/concurrent/edge/atomic_markable_reference.rb @@ -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 diff --git a/spec/concurrent/atomic/atomic_markable_reference_spec.rb b/spec/concurrent/atomic/atomic_markable_reference_spec.rb new file mode 100644 index 000000000..6319893d2 --- /dev/null +++ b/spec/concurrent/atomic/atomic_markable_reference_spec.rb @@ -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