I’ve spent a significant amount of time building microservices in Go and Scala. I’ve also spent a lot of time building applications for Android leveraging RxJava. Each of the technologies tackles concurrency in their own regards. Go leverages goroutines (their version of coroutines) and channels (more specifically — CSP). Scala, using Akka, leverages Actors. Actors operate on a higher level abstraction of Reactive Streams. They handle many of the challenges of stream-based programming models.
I’ve come to notice the Reactive Streams model is powerful. It naturally embraces the semantics of streaming systems. But it requires a deep understanding of the semantics of onSubscribe
, onNext
, onError
, onComplete
, etc. Without this, it’s easy to find yourself in trouble.
Coroutines and Actors fall nicely into a world where higher-level abstractions handle the challenges of stream based programming models.
This is a collection of notes on approaching concurrency in Kotlin using Actors. I’ll show how to use Actors with a simple and self contained example. I’ll also build upon this example to demonstrate pitfalls and performance wins.
What is an Actor
There’s a lot of material on Actors, so I’ll gloss over the details here as a light refresher / quick intro. Actors are a departure from the typical concurrency model you may be familiar with. A threading model relies on sharing memory. This introduces a requirement for locks and synchronization. As a consequence, the logic becomes complex and difficult to maintain. It also becomes susceptible to bugs like:
- deadlocks
- starvation
- race conditions
Do not communicate by sharing memory; instead share memory by communicating ( — Effective Go)
Actors are the fundamental unit of computation. They encapsulate state, behavior, and a message queue (referred to as a mailbox). This fits in nicely with the concept of Single Responsibility — the Actor should perform a very specific function.
Typically, Actors are fully asynchronous with an unbounded mailbox. They’re designed to be location transparent and distributable by design. What I like about Kotlin’s approach is that it affords for defining the boundedness of the mailbox. Available behaviors are:
This means you can incorporate backpressure into the implementation — a nice advantage.
Actors also operate sequentially — processing messages from the mailbox one at a time.
Defining an Actor
I’ll start by defining the mailbox message type as Action
with two concrete types: Spin
and Done
.
Next, I’ll define the Actor. I’m using a Rendezvous
capacity mode for now (using a capacity of 0). This means the sender will suspend if the Actor is busy processing a message.
Lastly, I’ll define the spin
function which simulates a long running CPU intensive operation.
Communicating with Actors
Now that the foundation has been laid for the example, I’ll show how to communicate with the Actor.
The above snippet dispatches 1000 Spin messages to the Actor. Each Spin message receives an ID to observe it’s processing behavior. send
is a suspending function so it requires a coroutine. After sending the message, the UI is updated to indicate which Spin message was sent and the elapsed time.
There’s a number of things wrong with the above approach:
- A new coroutine is launched for each iteration (that’s 1000 coroutines!)
.forEach { s -> launch(UI) { actor.send(s) ... }}
- The first thing that is logged is
time=140
but the code snippet above would imply that is the last thing that should be logged. It should indicate how long the entire operation took.
This happens because there’s a new coroutine launched for each iteration. Each coroutine, except for the first coroutine, is suspended. The Actor is processing the first message. This allows the main thread to move to the last operation of logging the entire duration. - We’re not recording when the Spin operation completed — just that it was delivered to the Actor’s mailbox.
This is a subtle distinction but an important one to understand. Actors operate on the principle of “fire and forget”. The Actor will process these messages one at a time on theCommonPool
thread. - Coroutines are lightweight, but that’s a lot of short lived objects generated.
From the memory profile above, we see a sudden spike in memory utilization. This is from the generation of Spin messages and coroutines.
Signaling a Complete Operation with Actors
Let’s update the logic to communicate when the Actor has finished processing all messages.
Messages are delivered to an Actor’s mailbox sequentially. An Actor consumes them sequentially. We can leverage this principle of Actor’s to use the Done
message as a signal that tells us the Actor has processed all messages.
A CompletableDeferred
facilitates communication with the Actor. The Done
message carries this object and the Actor calls .complete
on it.
We wait for the Actor to deliver this signal by calling .await()
on the CompletableDeferred
object.
The approach of delivering a
Done
message to indicate completion is to demonstrate principles of Actors. There could be better alternatives in accomplishing this. One alternative would be to consider a hierarchal system of Actors, of which one or more may communicate back to the UI thread. I’ll follow up on more details about this in a separate post.
Now we can retrieve an accurate time of how long it took for the Actor to process all the messages we sent it.
Working more efficiently with Coroutines
There’s still the issue of creating a new coroutine for each message sent to the Actor. Let’s improve this.
Instead of launching 1001 coroutines (one for each item sent to the Actor), we’ve reduced this to just one. Although coroutines are very lightweight, this is a significant improvement. We’ve avoided creating a large number of short-lived objects. But in doing so, this reveals another problem. The Actor is constructed with a Rendezvous mailbox strategy. The send
function suspends while the Actor is busy processing a message. This problem always persisted. But you wouldn’t have noticed since a new coroutine was launched for each iteration. And upon each iteration, each coroutine was in a suspended state. You can see this behavior from the log output below:
From the memory profile above, we see an improvement in memory utilization. Memory utilization ramps up gradually. The number of coroutines created (not shown above) is two — one for the UI and one for the Actor.
Distributing workload with Actors
So far we’ve been processing the work on a single Actor. Let’s see how we can improve things by distributing the workload across multiple Actors. This is where I think the Actor system really shines.
The parent Actor now becomes a router. It examines the incoming message and determines which child Actor to dispatch the message to. The child Actors all perform the same functionality but each operates on it’s own coroutine. The distribution strategy employs a simple round robin.
Signaling completion is interesting. Each child Actor could complete it’s workload at a different time. To account for this, each child Actor receives a Done
message with an ack to signal completion. The parent Actor awaits signal from each of the children. Once the signal is received from all it’s children, it signals the completion to the sender.
We gain a boost in performance. However, the one caveat to this approach is order of work is no longer maintained.
Conclusion
Traditional multi-threaded programming that requires processing of multiple tasks can be difficult. It involves synchronization of data using low-level constructs. Actors, however, operate at a higher abstraction. They provide a framework for describing a workflow through messages. You don’t need to think in terms of shared mutable state, threads, locks, or concurrent collections. Instead, you’re encouraged to think in terms of messages and communicating state through these messages.
The example above offered an introduction to another approach of handling concurrency in Kotlin. We looked at how to communicate with an Actor efficiently and how to take advantage of distributing workload across multiple Actors. Treat it as another tool in your toolbox.
Source code: https://github.com/jsaund/ActorsPlayground