Last few months I use Actors quite intensive, and found approach which works good for myself. So I'd like to share it and know what others think about it.
Let's consider the simple Counter actor, which has unlimited number of counters identified by name. So, I define an actor class like this:
const class CounterActor : Actor
{
//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////
new make(ActorPool pool := ActorPool()) : super(pool) {}
Locals-backed properties:
//////////////////////////////////////////////////////////////////////////
// Actor.locals-backed properties
//////////////////////////////////////////////////////////////////////////
private Str:Int counters
{
get { locals.getOrAdd("counters") |->Obj| { [:] } }
set { locals["counters"] = it } //setter just for illustration, not needed here
}
Public API (actor just sends message to self):
//////////////////////////////////////////////////////////////////////////
// Public API
//////////////////////////////////////////////////////////////////////////
Str:Int getAll() { send(GetCounters.instance).get }
Int get(Str name) { getAll[name] }
Void set(Str name, Int value) { send(SetCounter(name, value)).get}
Void incr(Str name, Int value := 1) { send(IncrCounter(name, value)) }
Void reset() { send(ClearAll.instance) }
Receive method:
//////////////////////////////////////////////////////////////////////////
// Overriden methods
//////////////////////////////////////////////////////////////////////////
override Obj? receive(Obj? msg)
{
if(msg is GetCounters) return processGet
else if(msg is ClearAll) processClear
else if(msg is IncrCounter) processIncr(msg)
else if(msg is SetCounter) processSet(msg)
return null
}
internal const class GetCounters
{
private new make() {}
const static GetCounters instance := GetCounters()
}
internal const class SetCounter
{
const Str key
const Int val
new make(Str key, Int val := 0)
{
this.key = key
this.val = val
}
}
internal const class IncrCounter
{
new make(Str key, Int val := 1)
{
this.key = key
this.val = val
}
const Str key
const Int val
}
internal const class ClearAll
{
private new make() {}
const static ClearAll instance := ClearAll()
}
When necessary, it is still possible to return Future from API method or add Bool sync := false parameter
For particular counter actor this example may look too heavy, but in more complex cases the usage convenience overweights boilerplate code from my point of view
jodastephenWed 10 Feb 2010
Thanks for the writeup.
My reaction is what a lot of code!
Firstly, the message classes demonstrate why we need simple state/bean classes ( SetCounter, IncrCounter), such as state class. `http://fantom.org/sidewalk/topic/378`
internal const state class SetCounter {
const Str key
const Int val := 0
}
I'd also suggest that there should be an easy way to create singletons ( GetCounters, CountAll), either by making the default constructor private or adding singleton class types. `http://fantom.org/sidewalk/topic/404`
internal const singleton class ClearAll {
}
And perhaps the properties stored in locals should be genuine fields as far as the code looks, but in an actor class type:
const actor class CounterActor {
new make(ActorPool pool := ActorPool()) : super(pool) {}
private Str:Int counters = [:]
...
Overall however, I still think that being able to define shared concurrent variables as an alternative to actors is useful. This example is just AtomicInteger in Java and doesn't need a complex actor/threaded backend - the same is true for ConcurrentHashMap. As constructs, the Java concurrent classes will be a lot easier for newcomers, and avoid forcing new starters to learn actors, which is a good thing.
brianWed 10 Feb 2010
Nice write-up ivan.
A technique I use myself a lot is to just use a list as tuple with the first item being the dispatch key:
Obj foo(Str a) { send(["doFoo", a]).get }
Obj bar(Int a, Int b) { send(["doBar", a, b].get }
Sometimes, I even just take that and dispatch with reflection internally:
Obj receive(Obj[] msg)
{
field(msg.first).callOn(this, msg[1..-1])
}
Obj doFoo(Str a) { ... }
Obj doBar(Int a, Int b) { ... }
Another technique I use is to perform error checking on the caller's thread before I send the message to the actor for processing.
Of course there is lots of opportunities for building libraries and frameworks on top of the core. Key thing is to ensure our foundation is solid.
ivanWed 10 Feb 2010
Thanks brian!
Very nice technique!
One of possible variations:
override Obj? receive(Obj? msg) { callOn(msg) }
private Obj? callOn(Obj?[] msg) { msg.first->callOn(this, msg[1..-1]) }
Void something(Str a, Int b) { send([#doSomething, a, b].toImmutable) }
private Void doSomething(Str a, Int b) { ... }
ivanFri 2 Jul 2010
Going back to this topic - I wrote a base class which I use extensively for my actors now. Example:
const class MyActor : ReflectActor
{
new make(ActorPool pool := ActorPool()) : super(pool) {}
//////////////////////////////////////////////////////////////////////////
// Public API
//////////////////////////////////////////////////////////////////////////
Void someOp() { send([#doSomeOp, [,]].toImmutable) }
Future getSum(Int arg1, Int arg2)
{
send([#doGetSomething, [arg1, arg2]].toImmutable)
}
Int getSumNow(Int arg1, Int arg2) { getSum(arg1, arg2).get }
//////////////////////////////////////////////////////////////////////////
// Message handlers
//////////////////////////////////////////////////////////////////////////
protected Int doGetSum(Int a, Int b) { a + b }
protected Void doSomeOp() {}
}
ReflectActor:
const class ReflectActor : Actor
{
new make(ActorPool pool) : super(pool) {}
override Obj? receive(Obj? msg)
{
if(msg == null) throw ArgErr("Null messages are not supported")
if(msg isnot Obj[]) throw ArgErr("Unsupported message type $msg.typeof")
list := msg as Obj?[]
method := list.first as Method
args := list.last as Obj?[]
if(list.size != 2 || method == null
|| args == null)
throw ArgErr("List must have two elements - method and list of args")
return method.callOn(this, args)
}
}
rfeldmanFri 2 Jul 2010
Huh. So this sort of gives an Actor the feel of a Java Thread that takes a Runnable.
ivan Wed 10 Feb 2010
Last few months I use Actors quite intensive, and found approach which works good for myself. So I'd like to share it and know what others think about it.
Let's consider the simple Counter actor, which has unlimited number of counters identified by name. So, I define an actor class like this:
Locals-backed properties:
Public API (actor just sends message to self):
Receive method:
Message processors:
Internal message classes (quite trivial):
After that, usage is very straightforward:
Few notes:
Bool sync := false
parameterjodastephen Wed 10 Feb 2010
Thanks for the writeup.
My reaction is what a lot of code!
Firstly, the message classes demonstrate why we need simple state/bean classes (
SetCounter
,IncrCounter
), such asstate class
. `http://fantom.org/sidewalk/topic/378`I'd also suggest that there should be an easy way to create singletons (
GetCounters
,CountAll
), either by making the default constructor private or addingsingleton class
types. `http://fantom.org/sidewalk/topic/404`And perhaps the properties stored in
locals
should be genuine fields as far as the code looks, but in anactor class
type:Overall however, I still think that being able to define shared concurrent variables as an alternative to actors is useful. This example is just AtomicInteger in Java and doesn't need a complex actor/threaded backend - the same is true for ConcurrentHashMap. As constructs, the Java concurrent classes will be a lot easier for newcomers, and avoid forcing new starters to learn actors, which is a good thing.
brian Wed 10 Feb 2010
Nice write-up ivan.
A technique I use myself a lot is to just use a list as tuple with the first item being the dispatch key:
Sometimes, I even just take that and dispatch with reflection internally:
Another technique I use is to perform error checking on the caller's thread before I send the message to the actor for processing.
Of course there is lots of opportunities for building libraries and frameworks on top of the core. Key thing is to ensure our foundation is solid.
ivan Wed 10 Feb 2010
Thanks brian!
Very nice technique!
One of possible variations:
ivan Fri 2 Jul 2010
Going back to this topic - I wrote a base class which I use extensively for my actors now. Example:
ReflectActor:
rfeldman Fri 2 Jul 2010
Huh. So this sort of gives an Actor the feel of a Java Thread that takes a Runnable.