223 lines
8.9 KiB
Ruby
223 lines
8.9 KiB
Ruby
require 'concurrent/atomic/atomic_reference'
|
|
require 'concurrent/collection/copy_on_notify_observer_set'
|
|
require 'concurrent/concern/observable'
|
|
require 'concurrent/synchronization'
|
|
|
|
# @!macro thread_safe_variable_comparison
|
|
#
|
|
# ## Thread-safe Variable Classes
|
|
#
|
|
# Each of the thread-safe variable classes is designed to solve a different
|
|
# problem. In general:
|
|
#
|
|
# * *{Concurrent::Agent}:* Shared, mutable variable providing independent,
|
|
# uncoordinated, *asynchronous* change of individual values. Best used when
|
|
# the value will undergo frequent, complex updates. Suitable when the result
|
|
# of an update does not need to be known immediately.
|
|
# * *{Concurrent::Atom}:* Shared, mutable variable providing independent,
|
|
# uncoordinated, *synchronous* change of individual values. Best used when
|
|
# the value will undergo frequent reads but only occasional, though complex,
|
|
# updates. Suitable when the result of an update must be known immediately.
|
|
# * *{Concurrent::AtomicReference}:* A simple object reference that can be
|
|
# atomically. Updates are synchronous but fast. Best used when updates a
|
|
# simple set operations. Not suitable when updates are complex.
|
|
# {Concurrent::AtomicBoolean} and {Concurrent::AtomicFixnum} are similar
|
|
# but optimized for the given data type.
|
|
# * *{Concurrent::Exchanger}:* Shared, stateless synchronization point. Used
|
|
# when two or more threads need to exchange data. The threads will pair then
|
|
# block on each other until the exchange is complete.
|
|
# * *{Concurrent::MVar}:* Shared synchronization point. Used when one thread
|
|
# must give a value to another, which must take the value. The threads will
|
|
# block on each other until the exchange is complete.
|
|
# * *{Concurrent::ThreadLocalVar}:* Shared, mutable, isolated variable which
|
|
# holds a different value for each thread which has access. Often used as
|
|
# an instance variable in objects which must maintain different state
|
|
# for different threads.
|
|
# * *{Concurrent::TVar}:* Shared, mutable variables which provide
|
|
# *coordinated*, *synchronous*, change of *many* stated. Used when multiple
|
|
# value must change together, in an all-or-nothing transaction.
|
|
|
|
|
|
module Concurrent
|
|
|
|
# Atoms provide a way to manage shared, synchronous, independent state.
|
|
#
|
|
# An atom is initialized with an initial value and an optional validation
|
|
# proc. At any time the value of the atom can be synchronously and safely
|
|
# changed. If a validator is given at construction then any new value
|
|
# will be checked against the validator and will be rejected if the
|
|
# validator returns false or raises an exception.
|
|
#
|
|
# There are two ways to change the value of an atom: {#compare_and_set} and
|
|
# {#swap}. The former will set the new value if and only if it validates and
|
|
# the current value matches the new value. The latter will atomically set the
|
|
# new value to the result of running the given block if and only if that
|
|
# value validates.
|
|
#
|
|
# ## Example
|
|
#
|
|
# ```
|
|
# def next_fibonacci(set = nil)
|
|
# return [0, 1] if set.nil?
|
|
# set + [set[-2..-1].reduce{|sum,x| sum + x }]
|
|
# end
|
|
#
|
|
# # create an atom with an initial value
|
|
# atom = Concurrent::Atom.new(next_fibonacci)
|
|
#
|
|
# # send a few update requests
|
|
# 5.times do
|
|
# atom.swap{|set| next_fibonacci(set) }
|
|
# end
|
|
#
|
|
# # get the current value
|
|
# atom.value #=> [0, 1, 1, 2, 3, 5, 8]
|
|
# ```
|
|
#
|
|
# ## Observation
|
|
#
|
|
# Atoms support observers through the {Concurrent::Observable} mixin module.
|
|
# Notification of observers occurs every time the value of the Atom changes.
|
|
# When notified the observer will receive three arguments: `time`, `old_value`,
|
|
# and `new_value`. The `time` argument is the time at which the value change
|
|
# occurred. The `old_value` is the value of the Atom when the change began
|
|
# The `new_value` is the value to which the Atom was set when the change
|
|
# completed. Note that `old_value` and `new_value` may be the same. This is
|
|
# not an error. It simply means that the change operation returned the same
|
|
# value.
|
|
#
|
|
# Unlike in Clojure, `Atom` cannot participate in {Concurrent::TVar} transactions.
|
|
#
|
|
# @!macro thread_safe_variable_comparison
|
|
#
|
|
# @see http://clojure.org/atoms Clojure Atoms
|
|
# @see http://clojure.org/state Values and Change - Clojure's approach to Identity and State
|
|
class Atom < Synchronization::Object
|
|
include Concern::Observable
|
|
|
|
safe_initialization!
|
|
attr_atomic(:value)
|
|
private :value=, :swap_value, :compare_and_set_value, :update_value
|
|
public :value
|
|
alias_method :deref, :value
|
|
|
|
# @!method value
|
|
# The current value of the atom.
|
|
#
|
|
# @return [Object] The current value.
|
|
|
|
# Create a new atom with the given initial value.
|
|
#
|
|
# @param [Object] value The initial value
|
|
# @param [Hash] opts The options used to configure the atom
|
|
# @option opts [Proc] :validator (nil) Optional proc used to validate new
|
|
# values. It must accept one and only one argument which will be the
|
|
# intended new value. The validator will return true if the new value
|
|
# is acceptable else return false (preferrably) or raise an exception.
|
|
#
|
|
# @!macro deref_options
|
|
#
|
|
# @raise [ArgumentError] if the validator is not a `Proc` (when given)
|
|
def initialize(value, opts = {})
|
|
super()
|
|
@Validator = opts.fetch(:validator, -> v { true })
|
|
self.observers = Collection::CopyOnNotifyObserverSet.new
|
|
self.value = value
|
|
end
|
|
|
|
# Atomically swaps the value of atom using the given block. The current
|
|
# value will be passed to the block, as will any arguments passed as
|
|
# arguments to the function. The new value will be validated against the
|
|
# (optional) validator proc given at construction. If validation fails the
|
|
# value will not be changed.
|
|
#
|
|
# Internally, {#swap} reads the current value, applies the block to it, and
|
|
# attempts to compare-and-set it in. Since another thread may have changed
|
|
# the value in the intervening time, it may have to retry, and does so in a
|
|
# spin loop. The net effect is that the value will always be the result of
|
|
# the application of the supplied block to a current value, atomically.
|
|
# However, because the block might be called multiple times, it must be free
|
|
# of side effects.
|
|
#
|
|
# @note The given block may be called multiple times, and thus should be free
|
|
# of side effects.
|
|
#
|
|
# @param [Object] args Zero or more arguments passed to the block.
|
|
#
|
|
# @yield [value, args] Calculates a new value for the atom based on the
|
|
# current value and any supplied arguments.
|
|
# @yieldparam value [Object] The current value of the atom.
|
|
# @yieldparam args [Object] All arguments passed to the function, in order.
|
|
# @yieldreturn [Object] The intended new value of the atom.
|
|
#
|
|
# @return [Object] The final value of the atom after all operations and
|
|
# validations are complete.
|
|
#
|
|
# @raise [ArgumentError] When no block is given.
|
|
def swap(*args)
|
|
raise ArgumentError.new('no block given') unless block_given?
|
|
|
|
loop do
|
|
old_value = value
|
|
new_value = yield(old_value, *args)
|
|
begin
|
|
break old_value unless valid?(new_value)
|
|
break new_value if compare_and_set(old_value, new_value)
|
|
rescue
|
|
break old_value
|
|
end
|
|
end
|
|
end
|
|
|
|
# Atomically sets the value of atom to the new value if and only if the
|
|
# current value of the atom is identical to the old value and the new
|
|
# value successfully validates against the (optional) validator given
|
|
# at construction.
|
|
#
|
|
# @param [Object] old_value The expected current value.
|
|
# @param [Object] new_value The intended new value.
|
|
#
|
|
# @return [Boolean] True if the value is changed else false.
|
|
def compare_and_set(old_value, new_value)
|
|
if valid?(new_value) && compare_and_set_value(old_value, new_value)
|
|
observers.notify_observers(Time.now, old_value, new_value)
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
# Atomically sets the value of atom to the new value without regard for the
|
|
# current value so long as the new value successfully validates against the
|
|
# (optional) validator given at construction.
|
|
#
|
|
# @param [Object] new_value The intended new value.
|
|
#
|
|
# @return [Object] The final value of the atom after all operations and
|
|
# validations are complete.
|
|
def reset(new_value)
|
|
old_value = value
|
|
if valid?(new_value)
|
|
self.value = new_value
|
|
observers.notify_observers(Time.now, old_value, new_value)
|
|
new_value
|
|
else
|
|
old_value
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# Is the new value valid?
|
|
#
|
|
# @param [Object] new_value The intended new value.
|
|
# @return [Boolean] false if the validator function returns false or raises
|
|
# an exception else true
|
|
def valid?(new_value)
|
|
@Validator.call(new_value)
|
|
rescue
|
|
false
|
|
end
|
|
end
|
|
end
|