[ANN] Actors 0.2, GenServers and Guards

I’m happy to announce the release of Actors 0.2 together with two actor infrastructure libraries GenServers and Guards.

Actors: Concurrency based on the Actor Model

Actors implements the classical Actor model in Julia. It

  • builds on Julia’s concurrency primitives,
  • provides a message based programming model for making concurrency easy to understand and reason about and
  • integrates well with Julia’s features for multi-threading and distributed computing.

Actors expresses actor behavior as a function or functor applied partially to some (acquaintance) arguments. The behavior gets executed with further (communication) arguments from an arriving message. That means Actors makes it easy to build function servers.

Ping-pong

Let me show you that with a game played concurrently:

using Actors, Printf, Random
import Actors: spawn

struct Player{S,T}
    name::S
    capa::T
end

struct Ball{T,S,L}
    diff::T
    name::S
    from::L
end

struct Serve{L}
    to::L
end

function (p::Player)(prn, b::Ball)
    if p.capa ≥ b.diff
        send(b.from, Ball(rand(), p.name, self()))
        send(prn, p.name*" serves "*b.name)
    else
        send(prn, p.name*" looses ball from "*b.name)
    end
end
function (p::Player)(prn, s::Serve)
    send(s.to, Ball(rand(), p.name, self()))
    send(prn, p.name*" serves ")
end

A player is a functor with two methods. If he can handle a ball, he sends it back, if it is too difficult, he looses it.

Now let’s play: In order to get reproducible results we initialize our random generator on each thread and start the players on assigned threads.

The print server prn gets an anonymous function as behavior. We start two players ping and pong with the print server’s link as acquaintance. We start the game by sending ping the :serve command and the address of pong:

@threads for i in 1:nthreads()
    Random.seed!(2021+threadid())
end

prn = spawn(s->print(@sprintf("%s\n", s))) 
ping = spawn(Player("Ping", 0.8), prn, thrd=3)
pong = spawn(Player("Pong", 0.75), prn, thrd=4)

send(ping, Serve(pong))

We get

Ping serves 
Pong serves Ping
Ping serves Pong
Pong serves Ping
Ping serves Pong
Pong looses ball from Ping

Actors has much more. Please look at it. Documentation and examples are nice. Not least it provides a modern interface to actors based on a messaging protocol. That can be enhanced by further libraries.

Guards: Servers for Mutable Variables

Guards is an Actors protocol for serving mutable variables to multiple threads and distributed workers so that they can be accessed concurrently. It has a nice interface and can work almost behind the scenes.

GenServers: Generic Servers

With GenServers you can easily implement your own server. You write an implementation module with an interface and callback functions based on a template, start a :genserver actor with it and you have server for your thing, which can be safely accessed from multiple threads and distributed workers.

What is Still Missing?

For becoming more mature

  1. we need yet to work on the interface between actor libraries,
  2. we must work on error handling, actor supervision and monitoring.

Those are next steps. Please try it out, tell what you think of it and join the journey.

Edit: improved the ping-pong example after a hint from @tisztamo

15 Likes

Wow, @pbayer, you’ve been on a coding spree these last weeks!

Yes, that has been quite a journey. Due to pandemic times, I could work full-time on it. But

  • I could take quite something from YAActL and
  • creating those two infrastructure libraries has been easy due to the new Actors protocol and
  • it was also fun :wink:.
3 Likes

Whoah!

I love this package! It’s great to be able to specify which thread or process id to launch Actors onto.

Question: I’ve tried changing

send(prn, p.name*" looses ball from "*b.name)

to

send(prn, p.name*" looses ball from "*b.from.name)

where the ball sender’s name is accessed as a field on from rather than straight from the ball itself. The output then becomes:

Ping serves

only and no game is played (or at least printed to the terminal). Why is this?

glad to hear that you love it!

In such a case you can do

julia> Actors.info(pong)
Task (failed) @0x00000001153186d0
type Link has no field name
getproperty(::Link{Channel{Any}}, ::Symbol) at ./Base.jl:33
(::Player{String,Float64})(::Link{Channel{Any}}, ::Ball{Float64,String,Link{Channel{Any}}}) at /Users/paul/.julia/dev/Actors/examples/pingpong.jl:30
....

The point is that from is a link to the actor. You cannot subset it.

1 Like

Thanks - I understand now. I’ve found Actors very easy to build a mental model around.

One further question: The second argument, prn, in ping = spawn(Player("Ping", 0.8), prn, thrd=3) is a another spawned Link, which appears to then be the first argument to calls to the Player functors. Is this how additional arguments to functors are configured?

I have a slightly more generic question about actors. I can easily see how they can be used for instance for handling streaming data, but can they be used to build algorithms of some kind?

Yes, prn is an additional “acquaintance” argument. The actor holds that in its state. You need to send it only the missing arguments to cause it to execute the behavior (functor).

If you do spawn(Player("Ping", 0.8), prn, thrd=3), you partially apply the Player functor with its acquaintances "Ping", 0.8 to one further acquaintance prn. Then later it gets the Ball as communication argument.

1 Like

What spontaneously comes to my mind, is the following:

  1. Actors can operate as function servers. They can do a calculation in parallel on some other thread or worker and serve it to your algorithm. In this regard they are similar to tasks.
  2. But actors hold also state. Therefore they can serve also variables. If you have an algorithm needing access to a variable - maybe in parallel on multiple threads or workers - you can have an actor serving it. This is a composable alternative to a lock. It doesn’t need a lock, but cares the same that the variable is accessed strictly sequentially.
  3. Then I think, actors are great to model stochastic processes and systems.
  4. You can model with actors the typical concurrency conundrums: dining philosophers, sleeping barber, producer-consumer problem … Also those are algorithms.
3 Likes

Some further thoughts on this:

Those two features give the possibility to compose actors hierarchically and sequentially into algorithms as we normally expect. As I mentioned actors being lock free are better composable. But otherwise you may not gain much with actors for hierarchical and sequential composition.

But a whole new game is to compose actors asynchronously. That gives you the access to

and much more. For those applications actors usually give you a programming model easier to understand and reason about.

For distributed algorithms you may be interested to look at Distributed Algorithms for Message-Passing Systems.

2 Likes

Today in the Multithreading BoF I gave a presentation on Actors.jl. The slides are at:

enjoy!

5 Likes

Great!

What is the Multithreading BoF?

1 Like

It is an event every three weeks in the Julia calendar and organized by @jameson via the Julia Slack multithreading channel.

3 Likes