How are Akka actors different from Go channels?

Discussion thread on

From the question How are Akka actors different from Go channels?

I have worked extensively with both Akka actors and Go channels/goroutines and would like to offer a few comments.

Firstly, it is important to stress that CSP allows rendezvous-based synchronisation between goroutines using channels, and channels can optionally include buffering.

When one goroutine attempts to send on an unbuffered channel, it checks whether the other party is present. If not, it sits waiting, during which it consumes no CPU and only a little memory. When the second goroutine arrives at the rendezvous point, data is exchanged then they go their separate ways.

Aside: this concept of rendezvous is quite similar in Ada (also based on Hoare's work), but Ada does not have channels to separate tasks so they must meet explicitly.

For an unbuffered channel, the rendezvous always causes one of the goroutines to wait for the other. When channels have buffering, there are three cases. If the buffer is empty, then the reading goroutine still has to wait for data from a sending goroutine. If the buffer is full, then the sending goroutine has to wait for space before it can complete the rendezvous. In the other case, the buffer is part-full so the reader can always read immediately without waiting, and the writer can always write without waiting.

In Go, buffered channels are always finite in size. Although it is easy to code up a goroutine that acts as a limitless buffered pipe, the normal view is that using limitless buffering is a sign of smelly code, because in reality, memory is always finite.

Unbuffered channels always cause waiting; buffered channels may also cause waiting. The result is that there is flow control across the network of processes - I'll discuss this further later.

Go has a select statement for choosing between alternatives (the CSP choice) which may include channels, timers or simply skipping on to the next thing. This allows what one might call "waiting faster" by picking up from whichever channel is available first. But remember that whilst a goroutine is waiting on the channels (CSP calls them guards) in a select, it consumes no CPU and very little memory.

It is easy to describe Akka actors in terms of goroutines. Each actor is one goroutine that has a 'while true' loop ( for { } in Go) enclosing its logic. It has exactly one input channel, which it reads from at the start of every loop. This channel has infinite buffering (let's ignore for now that Go doesn't normally allow this). It also has an unbounded number of writers to the channel; the writer are also actors. In this Go model of Akka, select would never be used.

Note in passing that Go channel ends can be shared: there can be many writers at one end and many readers at the other end; Go sequentially interleaves messages in an arbitrary way.

It's clear from this model that goroutines and channels can be used to describe other network patterns. Essentially, Akka implements a fixed kind  network (of variable topology) in CSP terms. In terms of performance, it does it probably much more efficiently than the equivalent Go would do. One feature of this kind of network is that there is no explicit flow control because no write rendezvous is ever needed on an infinite buffer. The model is fully asynchronous.

Akka also has a superb feature of easily allowing actors to exist on different computers on their network. Message marshalling and routing is handled automatically.

But there are downsides to the actor model.

  • There are cases where flow control is needed. Although these can be implemented in Akka using handshake dialogues, the code gets messy and it's not efficient any more.
  • There is no ability of an actor to refuse to communicate. When a message arrives, it has to be processed or, perhaps, put into a buffer to be dealt with later. A goroutine with more than one channel attached can choose to be in states where it ignores a subset of its channels. The goroutine can select on all or just some of its channels, depending on its state. An example might be an implementation of a simple fixed-size queue goroutine with one input and one output channel. Initially it is empty, so it ignores its output channel. Ultimately, it could be full, when it would ignore its input channel. Otherwise it would select between both channels to decide what to do. Because it is possible for the goroutine to ignore either channel, its logic is expressed nice and simply.
  • Akka places full reliance on the infinite input buffers never running out of memory - but this is not guaranteed.

There is one important downside to the CSP way of networking concurrent behaviour:

  • Any network of goroutines that has some mutual dependency may deadlock. The developer must be aware of this and code so that deadlock is eliminated; this involves identifying mutual dependencies and altering them in some way (there are several well-known techniques, the easiest of these being to use a pure client-server pattern, which is known to be deadlock-free).

But consider what happens if a deadlocking network of goroutines is converted to Akka. The non-blocking communications means that deadlock won't happen - but how can we be sure that the queues will not overflow? There is a direct equivalence between deadlock and exhaustion of not-so-infinite buffers.

Goroutines only work within a single computer (albeit with any number of cores). There is no built-in networking, although external packages add this capability. This differs from Akka; it is also inferior to other CSP-based communications, such as JCSP for Java, which includes channels that operate transparently over network connections, retaining the normal synchronisation semantics.

One final note concerning goroutines is that their termination is arbitrary. Contrast this with Occam (another CSP language), in which processes are 'owned' by their parent, and the parent waits until all its child processes have terminated. It is possible to make Go behave in a more rigorous CSP way (like Occam) by adding synchronisation code explicitly.

comments powered by Disqus