Friday, December 09, 2005

ruby, singleton, locking, etc

I came across the following problem the other day: implementing the singleton pattern for a multi-threaded ruby app. Turns out that ruby has a singleton class that does just that...

require 'singleton'
class SingletonTest
  include Singleton
  def initialize
    puts 'start initialize '
    sleep 2
    puts 'end initialize'
  end
end

a = b = c = d = nil


t1 = Thread.new {
  a = SingletonTest.instance
  b = SingletonTest.instance
}

t2 = Thread.new {
  c = SingletonTest.instance
  d = SingletonTest.instance
}

t1.join
t2.join

puts a.to_s
puts b.to_s
puts c.to_s
puts d.to_s
This works perfect. No race conditions occur and only one instance of the object gets created. However I couldn't seem to figure out how to get the object to use anything besides that default initialize method without any arguments. I needed something that would accept values that would be used when the object is first created. I'm not sure why I couldn't get the initialize method to accept arguments, it seems to me that by defining initialize myself would be enough but I guess not. So I went about implementing this myself...
class SingletonTest
  @@singleton_instance = nil
  def SingletonTest.get_instance
    puts 'getting instance'
    if(!@@singleton_instance)
      sleep 2
      puts '  instance created'
      @@singleton_instance = SingletonTest.new
    else
      puts '  returning already created instance'
    end
    return @@singleton_instance
  end
end

st2 = st1 = nil

t1 = Thread.new {st1 = SingletonTest.get_instance}
t2 = Thread.new {st2 = SingletonTest.get_instance}

t1.join
t2.join

puts st1.to_s
puts st2.to_s
This uses a class variable and method. It works fine in single threaded application, but feel flat on its face with multiple threads. So I started poking around with some of the locking mechanisms that ruby has... I started off with monitors:
require 'monitor'

class TestSingleton < Monitor
  @@test_object = nil
  
  def getInstance(s)
    synchronize do
      if !@@test_object
        return @@test_object = TestObject.new
      else
        return @@test_object
      end
    end
  end
end

class TestObject
  def initialize
  end
end
The problem I found with the ruby implementation of monitors is the fact you have to extent the monitor class and then create a new instance of the class before you can synchronize a block of code. So you can't use 'synchronize do' inside of a class method (unless perhaps you were dealing with an instance of the class), which just makes things messy here. So I threw out the monitor idea and tried using ruby's Mutex class:
require 'thread'

class SingletonTest
  @@mutex = Mutex.new
  @@object_instance = nil
  def SingletonTest.get_instance s
    puts 'getting instance '+s.to_s
    @@mutex.synchronize do
      if !@@object_instance
        puts '  creating instance'
        sleep 2
        @@object_instance = SingletonTest.new
      end
      return @@object_instance
    end
  end
end

st1 = st2 = st3 = st4 = nil

t1 = Thread.new {st1 = SingletonTest.get_instance '1'}
t2 = Thread.new {st2 = SingletonTest.get_instance '2'}

t1.join
t2.join

puts st1.to_s
puts st2.to_s
Instead of extending a class like monitors, you just create an instance of Mutex and use it to create synchronized blocks of code. This seems to work for me. I'm not sure that this is the best implementation, but until I find something better this will do.

0 Comments:

Post a Comment

<< Home