Kotlin Concurrency with Actors

Jag Saund
7 min readJun 13, 2018

--

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.

Actor Message Definitions

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.

Actor Definitions

Lastly, I’ll define the spin function which simulates a long running CPU intensive operation.

Spin function simulates a long running CPU intensive process

Communicating with Actors

Now that the foundation has been laid for the example, I’ll show how to communicate with the Actor.

Send 1000 Spin Messages to the Actor and update UI

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.

Log output of dispatching and processing Spin messages to Actor

There’s a number of things wrong with the above approach:

  1. A new coroutine is launched for each iteration (that’s 1000 coroutines!)
    .forEach { s -> launch(UI) { actor.send(s) ... }}
  2. 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.
  3. 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 the CommonPool thread.
  4. Coroutines are lightweight, but that’s a lot of short lived objects generated.
Memory profile with 1000 coroutines

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.

Communicating with an Actor to indicate an operation has completed

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.

Log output of using a Done message to communicate completion

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.

Launch one coroutine

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:

Log output highlighting behavior of Rendezvous mailbox strategy
Memory profile after reducing to 1 coroutine

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.

Distributing workload across multiple actors

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.

Log output shows out of order processing of messages

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

--

--

Jag Saund

Android Engineer || ex Periscope, Amazon, Upthere || Sometimes a Photographer (to get me away from my computer)