#1186 Singleton and Actors

yachris Sat 28 Aug 2010

Hello,

Trying to do a singleton map:

using concurrent

const class SingletonMap : Actor
{
    static SingletonMap getInstance()
    {
        return instance
    }

    Int? getValueForKey(Str key)
    {
        [Str:Int]? map := Actor.locals["map"]
        if (map == null)
        {
            map = [:]
            Actor.locals["map"] = map
        }
        return map[key]
    }

    Void setValueForKey(Str key, Int val)
    {
        [Str:Int]? map := Actor.locals["map"]
        if (map == null)
        {
            map = [:]
            Actor.locals["map"] = map
        }
        map[key] = val
    }

    Void resetForTestingOnly()
    {
        Actor.locals["map"] = null
    }

    private new make(ActorPool p) : super(p)    
    {
    }

    private static const SingletonMap instance := SingletonMap(ActorPool())
}

Which works fine, as long as I'm only using one thread. Multiple Actors, of course, see different SingletonMap instances (!) since Actor.locals["map"] is per-thread storage.

Is there any way to implement a Singleton which is the same singleton for all threads?

Thanks!

vkuzkokov Sat 28 Aug 2010

We shall dispatch a message every time we want to access map. Messages to the same actor are guaranteed to be processed within one Actor's context, hence the name.

using concurrent


const class SingletonMap : Actor
{
  static SingletonMap getInstance()
  {
    return instance
  }
  private static [Str:Int] getMap()
  {
    if (Actor.locals["map"] == null)
    {
      Actor.locals["map"] = [Str:Int][:]
    }
    return Actor.locals["map"]
  }

  override protected Obj? receive(Obj? msg) {
    if (msg == null)
    {
      Actor.locals["map"] = null
    }
    if (msg is Str)
    {
      return getMap.get(msg)
    }
    if (msg is Obj[])
    {
      Obj[] ent := msg
      return getMap.set(ent[0], ent[1])
    }
    return null
  }

  Int? getValueForKey(Str key)
  {
    return send(key).get()
  }

  Void setValueForKey(Str key, Int val)
  {
    send([key,val])
  }

  Void resetForTestingOnly()
  {
    send(null)
  }

  private new make(ActorPool p) : super(p)    
  {
  }

  private static const SingletonMap instance := SingletonMap(ActorPool())
}

yachris Sat 28 Aug 2010

Hey vkuzkokov,

Thanks very much for the fix! Much appreciated. Good insight, too...

vkuzkokov Sat 28 Aug 2010

Here's solution I wrote first. It's little bit harder to comprehend. Though it's shorter and somewhat more elegant.

using concurrent

const class SingletonMap : Actor
{
  static SingletonMap getInstance()
  {
    return instance
  }
  private static [Str:Int] getMap()
  {
    if (Actor.locals["map"] == null)
    {
      Actor.locals["map"] = [Str:Int][:]
    }
    return Actor.locals["map"]
  }

  override protected Obj? receive(Obj? msg) {
    if (msg is |->Obj?|)
    {
      |->Obj?| proc := msg
      return proc()
    }
    Actor.locals["map"] = null
    return null
  }

  Int? getValueForKey(Str key)
  {
    return send(|->Obj?|{ return getMap.get(key) }).get()
  }

  Void setValueForKey(Str key, Int val)
  {
    send(|->Obj?|{ getMap.set(key, val) })
  }

  Void resetForTestingOnly()
  {
    send(null)
  }

  private new make(ActorPool p) : super(p)    
  {
  }

  private static const SingletonMap instance := SingletonMap(ActorPool())
}

What happens here is that we send a function which is executed in Actor's context. So if we had:

doSmth

We make it:

return send(|->Obj?|
  {
    doSmth
  }).get()

Also we omit return and get if we don't want to return.

brian Sat 28 Aug 2010

Here is what I would probably code as a basic singleton map example:

const class SingletonMap
{
  const static SingletonMap val := make()

  Obj? get(Str name) { actor.send(name).get(200ms) }

  Void set(Str name, Obj? val) { actor.send([name, val]) }

  private new make() 
  {
    actor = Actor(ActorPool()) |msg| { receive(msg) }
  }

  private Obj? receive(Obj? msg)
  {
    Str:Obj? map := Actor.locals.getOrAdd("map") { Str:Obj[:] }
    if (msg is Str)
      return map[msg]
    else 
      return map.set(msg->get(0), msg->get(1))
  }

  private const Actor actor
}

Couple comments:

  • I usually make the actor a private field instead of subclassing from Actor so that I don't expose that to the public API
  • I typically always include a timeout in my blocking sends to throw a TimeoutErr just in case something goes wrong

yachris Sat 2 Oct 2010

One minor change I made today:

const class SingletonMap
{
  const static SingletonMap val := make()

  Obj? get(Str name) { actor.send(name).get(200ms) }

  Void set(Str name, Obj? val) { actor.send([name, val].toImmutable) }  // note .toImmutable

  private new make() 
  {
    actor = Actor(ActorPool()) |msg| { receive(msg) }
  }

  private Obj? receive(Obj? msg)
  {
    Str:Obj? map := Actor.locals.getOrAdd("map") { Str:Obj[:] }
    if (msg is Str)
      return map[msg]
    else 
      return map.set(msg->get(0), msg->get(1))
  }

  private const Actor actor
}

The beauty of this is that, if you've got const objects you're storing, they (A) don't have to be @Serializable and (B) you'll get the exact same (i.e., ===) object out that you got in, for a specific key.

Login or Signup to reply.