Published using Google Docs
Ruby Memory Model - instance variables extension
Updated automatically every 5 minutes

Ruby Memory Model - instance variables extension

This instance variable extension builds on  Ruby Memory Model. All proposed changes are backward compatible.

Multiple issues will be opened or linked to cover this document (they’ll be linked here), they will be:

Following annotations are used (sometimes with additional comments) to simplify documentation of the methods in examples:

Volatile instance variables

Volatile has same meaning as in Java (C meaning differs), which also implies that it provides Sequential consistency. Weaker consistency models are not considered for now.

If the volatile keyword argument is true, it makes reads and writes to the variables volatile in all instances of the class. The user should first declare, which instance variables are volatile, then create instances. If the instance variable is already volatile or atomic it has no effect. Applies to already existing instances eventually as well (synchronisation is required to make the effect immediate). After marking a variable as a volatile, writes have volatile semantics such that all writes are immediately visible to subsequent volatile reads, from any thread. Applies to instance variable literals reads and writes. Other proposals may add more keyword arguments. The keyword accepts only true as a value (or default nil, which has no effect). (By having a default value nil and leaving false free, it keeps a door open for adding relaxation later.) Returns names of newly defined methods (array of Symbols), requires 11541.

class Module

 # @synchronization_required of class and/or already initialized instances

 # @safe

 #

 # MRI: No action is needed. All instance variables are volatile because of

 # the semantics of the GIL.

 #

 # JRuby: Switching from normal variable accessors to a volatile ones does not

 # have to be a volatile write since this method specifies that the instance

 # has to be safely published by user after a instance variable is made

 # volatile, which ensures that subsequent reads will see volatile variable

 # accessor and therefore it will do volatile reads/writes.

 #

 # JRuby+Truffle: user synchronizes the volatile variable declaration

 # which propagates the shape change to a shape with a volatile field (or an

 # atomic wrapper) to subsequent object readers.

 def self.attr(*names, reader: false, writer: false, volatile: nil)

   # It keeps the old attr(:name, false) signature for compatibility.

   # Would be nice if default of `reader:` could be made false to make

   # declaration of volatility without readers/writers easy:

   # `attr :name, volatile: true`.

   #

   # ... define readers/writers if applicable

   #

   # ... mark variables as volatile if applicable

 end

 # `attr_reader`, `attr_writer`, `attr_accessor` could also accept volatile

 # and other key-word arguments but it might be confusing, since it can be

 # understood as only the reader (or writer) is made volatile.

 # Therefore only attr accepts volatile and other keyword arguments.

end

Methods instance_variable_get and instance_variable_set perform volatile read and write operations if the variable is marked as volatile. They do normal read and write operations if it’s non-volatile.

Atomic instance variables

Atomic variables extend volatile variables with few atomic operations like compare-and-set. All methods are thread-safe. All assignments are already considered atomic, the name is chosen because of the atomic CAS helpers.

If the atomic keyword argument is true, it makes the instance variable volatile and atomic in all instances of the class, and is therefore a superset of volatile: true. It defines atomic helpers for each instance variable. If the variable is already atomic, it redefines the helpers (to match behavior of attr_reader which redefines readers as well). Returns names of newly defined methods (array of Symbols). Proposes to add another keyword atomic to attr* methods.

The comparison uses referential equality, numbers with the same value are not expected to be represented by same objects. This comment explains why it’s strong enough.

class Module

 # @synchronization_required

 # @safe

 def self.attr(*names, reader: false, writer: false, volatile: nil, atomic: nil)

   # All atomic instance variables are also volatile.

   volatile ||= atomic

   # ... content of attr method in volatile extension

   if atomic

     # It defines atomic helpers for each instance variable.

     names.each do |variable_name|

       name = variable_name[1..-1] # strip leading '@'

       class_eval <<-RUBY, __FILE__, __LINE__ + 1

         # By default these atomic helpers are private, since they are almost

         # always used only internally.

         private

         # It gets an old value and sets a new one, returns old value

         # @safe

         # @atomic

         # @volatile

         def get_and_set_#{name}(value)

           # ...

         end

         # It compares a current value and an expected_value

         # and if they are equal (#equal?) sets the new value atomically. It

         # returns the result of the comparison.

         # @safe

         # @atomic

         # @volatile

         def compare_and_set_#{name}(expected_value, new_value) # => true or false

           # ...

         end

         # It compares the current value and an expected_value

         # and if they are equal sets the new value. It returns the current_value

         # which is either old non-matching value or a new_value.

         # @safe

         # @atomic

         # @volatile

         def compare_and_exchange_#{name}(expected_value, new_value)

           # ...

         end

         # It computes a new value based on old_value using the supplied block

         # should be short as possible to avoid looping.

         # It returns a pair of old_value used for computation

         # and a newly computed value.

         # @safe

         # @atomic

         # @volatile

         def update_#{name}(&compute_new_value)

           while true

             # volatile read

             current_value = @#{name}

             new_value = compute_new_value.call(current_value)

             break if compare_and_set_#{name}(current_value, new_value)

           end

           # Return which old_value was used and what was the resulting new value.

           [current_value, new_value]

         end

       RUBY

     end

   end

   # MRI: requires the CAS helpers to be implemented. E.g. as in C extension

   # for AtomicReference in concurrent-ruby gem.

   #

   # JRuby and JRuby+Truffle: has to implement the helpers as well.

   # ... return names of all defined methods

 end

 # As with volatile, attr_reader and similar do not accept :atomic keyword.

end

Relaxation of the instance variables

If previously atomic and atomic keyword is false it un-defines atomic helpers. If previously volatile and volatile keyword is false it turns previously declared volatile or atomic variable into a relaxed one (non-volatile semantics), also un-defines atomic helpers if applicable. Can make variable relaxed in a class even though its parent has the variable volatile or atomic (easy since they have different metaclasses). Added as a complement to volatile/atomic declarations to be able to undefine volatility as other things in Ruby can be undefined or removed. Useful for testing. It the keyword has default nil value it leaves the instance variables as is.

Final instance variable construction

Final instance variables are required to be able to create thread-safe immutable objects without additional synchronisation on instance variable reading. They are also required to be able to assign safely a mutex into an instance variable in constructor, to be able to protect other mutable instance variables.

This can be fixed with two approaches: explicit and implicit.

Explicit final instance variables

If the final keyword argument is true it makes the instance variables final. Meaning they can be assigned only once, additionally if they are assigned in constructor (the intended usual use) the class having any final variables makes sure that (if the object is properly constructed (reference does not escape)) final fields cannot be seen uninitialized when the object is shared after construction. Reassignment of a final instance variable raises an exception. They can still be reassigned with instance_variable_set without error though. Proposes to add another keyword final to attr* methods.

class Module

 # @safe

 # @synchronization_required

 def self.attr(*names, reader: true, writer: false, volatile: nil, atomic: nil,
             
final: nil)

   # ... previous volatile and atomic parts

   #

   # ALL: checking of assignments to final variables needs to be added

 end

end

class Class

 # MRI: does not have to do any additional steps to ensuring final fields

 # visibility.

 #

 # Other implementations: If the constructed instance has a final variable

 # a StoreStore memory barrier (or a stronger # barrier depending what is

 # available on a given platform) has to be inserted. It ensures that instance

 # variables assignments will not be reordered with subsequent sharing

 # of the newly created instance. In other words uninitialized instance

 # variables field cannot be observed.

 def self.new(*args, &block)

   obj = super(*args, &block)

 ensure

   store_store_memory_fence if obj.instance_variables(:final).any?

 end

end

Implicit final instance variables

Any variable assigned only once in constructor is guaranteed to be visible after the instance reference is published. Therefore a StoreStore memory barrier has to inserted after all Class.new method calls, since it has to be assumed that any instance variable assigned in constructor may not be assigned again.

Related changes

Few other updates which make the API nicer. Already proposed change in 11541.

class BasicObject

 # private, protected, public, private_class_method, protected_class_method,

 # public_class_method all also accept array of symbols to allow following

 # syntax, which makes all defined atomic helpers private methods.

 public attr :status, atomic: true

end

class Object

 # Returns array of instance variables matching supplied types, which may be:

 # :relaxed, :volatile, :atomic, :final. By default it returns all types.

 # @synchronization_required

 # @safe

 def instance_variables(*types)

   # ...

 end

end

Examples

# Reads are very cheap since they require only volatile read. It's guaranteed

# to have always up-to-date value because there is a volatile write inside add

# method.

class Counter

 # Assuming reader keyword has default value false, this declares only volatile

 # count and final mutex instance variables without readers or writers.

 attr :count, volatile: true

 attr :mutex, final: true

 def initialize(value)

   # volatile write

   @count = value

   # final value, which is always visible after object is constructed

   @mutex = Mutex.new

 end

 # There is a volatile write to count inside the mutex critical section in add

 # method, which synchronises with volatile read in this count method.

 # The mutex only synchronises with each other, it ensures that only one +=

 # operation is executed at any given time. It does not block readers.

 def count

   # volatile read, always latest value

   @count

 end

 def add(addition = 1)

   @mutex.synchronize do

     # += is not an atomic operation so it has to be protected by mutex

     # volatile write to make latest values visible in count method

     @count += addition

   end

 end

end

# Improved Counter using atomic helper

class CasCounter

 # Define atomic helpers for count variable, they are private by default

 attr :count, atomic: true

 # Define a public count reader, which does a volatile read since it’s declared

 # atomic therefore volatile. It’s public by default.

 attr_reader :count

 def initialize(value)

   # volatile write

   @count = value

 end

 def add(addition = 1)

   while true

     # volatile read, always latest value

     current = @counter

     # compute new value

     new     = current + addition

     # optimistically and atomically tries to set new value, it will succeed

     # if the actual value is still identical to current

     return new if compare_and_set_counter current, new

   end

 end

 # alternative add implementation using update helper with closure

 def alternative_add(addition = 1)

   _old, new = update_count { |current| current + addition }

   new

 end

end

# Global counter

#

# The class has to be first constructed with Class.new and then assigned to

# constant CountsInstances to make internal attr call which has

# @synchronization_required safely published through the volatile write

# to the constant variable CountsInstances (specified elsewhere).

# If regular class is used it assigns the constant and then it evaluates its

# body.

#

# Usually the CountsInstances = Class.new pattern can be avoided since classes

# are placed inside different files. require 'file' contains internal

# synchronization which ensures that the whole content of the file is visible

# after require 'file' finishes (specified elsewhere), which is sufficient to

# ensure visibility of global_count volatility.

CountsInstances = Class.new do

 # declaring global volatile counter

 singleton_class.send :attr, :global_count, volatile: true

 # volatile write

 MUTEX = Mutex.new

 # volatile write

 @global_count = 0

 def self.increase_count

   MUTEX.synchronize do

     # += is not an atomic operation so it has to be protected by mutex

     # volatile write

     @count += 1

   end

 end

 def self.count

   # volatile read

   @count

 end

 def initialize

   self.class.increase_count

   # ...

 end

 # ...

end

# Simple Future implementation, represents a value which will be fulfilled

# in future.

class Future

 PENDING = Object.new

 # define volatile instance variable to hold the value of the Future

 attr :value, volatile: true

 # define final variables for Mutex and ConditionVariable to be able to

 # block threads until future is complete.

 attr :lock, :condition, final: true

 def initialize

   # set final variables, they are guaranteed to be visible after

   # initialization

   @lock      = Mutex.new

   @condition = ConditionVariable.new

   # volatile write

   @value     = PENDING

 end

 # Reading the default value is volatile. Sometimes when we've already read the

 # value, we can use the argument to pass the value avoiding extra volatile

 # read.

 def complete?(value = @value)

   value != PENDING

 end

 # Returns the value of the Future, blocks threads if the value is not

 # fulfilled.

 def value

   # do volatile read only once

   value = @value

   # return immediately, if value is set using only one cheap volatile read

   return value if complete? value

   # Enter expensive critical section if the value is not set.

   @lock.synchronize do

     # Reread the value to make sure it's still un-fulfilled

     until complete?(value = @value)

       # blocks thread until it is broadcast when Future was fulfilled

       @condition.wait @lock

     end

   end

   # return the fulfilled value

   value

 end

 def fulfill(value)

   @lock.synchronize do

     # check that it wasn't already completed

     raise 'already fulfilled' if complete?

     # fulfill the value, volatile write

     @value = value

     # wake up all blocked threads

     @condition.broadcast

   end

   self

 end

end