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 normalStruct) 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::Structin 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.
STMhas an interface similar toProc(and is to_proc’able)- An
STMobject is roughly analagous to a Perl 6Codeobject markedis atomic STM.atomically {...}works likeSTM.new {...}.callsome_stm.or_else {...}works likesome_stm.or_else( STM.new {...} )STM#|will probably be an alias for the non-block form ofSTM#or_else- Since I promised reified monads, here’s monadic
pass(i.e. bind) andwrap(i.e. unit) forSTM(a more efficientpassis 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