Ruby STM: First Round

Audrey’s now implemented support for retry and retry_with (i.e. orElse) in Pugs, which got me thinking more about how STM in Ruby should work. Given my current design, I think this is what we can expect a Ruby port of Audrey’s example script to look like:

require 'stm'

state = STM::Struct.new( :a, :still_running ).new( 0, 2 )

t1 = Thread.new {
  puts "Thread 1 started" 
  STM.atomically {
    state.a > 5 or STM.retry
    state.a = -1000
    state.still_running -= 1
  }
  puts "Thread 1 finished: #{state.a} is >5 and reset to -1000" 
}

t2 = Thread.new {
  puts "Thread 2 started" 
  STM.atomically {
    STM.new {
      state.a > 100 or STM.retry
    }.or_else {
      state.a < -100 or STM.retry
    }.call
    state.still_running -= 1
  }
  puts "Thread 2 finished: #{state.a} is now < -100" 
}

while state.still_running.nonzero?
  puts state.a
  sleep 1
  STM.atomically { state.a += 1 }
end

t1.join
t2.join

Some miscellaneous notes:

  • Only objects of classes derived from STM::Struct (which works much like normal Struct) are transactional. This mainly avoids the need for major surgery on the Ruby runtime, allowing me to complete my prototype in pure Ruby.
  • Given that, however, there’s no way to prevent things with non-revertable side-effects being done within a transaction. That’s kind of unfortunate.
  • While we stored both variables in the same STM::Struct in the above example, we didn’t have to—it was just shorter to write. You should be able to use multiple struct instances in the same transaction. Otherwise it wouldn’t be very useful.
  • If we didn’t perform those joins in the master thread the “Thread N finished” messages wouldn’t necessarily get printed, since those threads would get killed as soon as the master thread exited. This isn’t peculiar to Ruby (the Perl 6 version has to do the same thing), but forgetting joins is a common mistake.
  • STM has an interface similar to Proc (and is to_proc’able)
  • An STM object is roughly analagous to a Perl 6 Code object marked is atomic
  • STM.atomically {...} works like STM.new {...}.call
  • some_stm.or_else {...} works like some_stm.or_else( STM.new {...} )
  • STM#| will probably be an alias for the non-block form of STM#or_else
  • Since I promised reified monads, here’s monadic pass (i.e. bind) and wrap (i.e. unit) for STM (a more efficient pass is probably possible, but depends on implementation details):
class STM
  def self.wrap( value ) ; new { value } ; end
  def pass( &block )
    STM.new { block.call( self.call ).call }
  end
end
hoodwink.d enhanced