DEV Community

Cover image for Using actors in Kotlin - a concurrent play in one act
Dominik Liebler
Dominik Liebler

Posted on • Originally published at domnikl.github.io on

Using actors in Kotlin - a concurrent play in one act

In february this year I already wrote about Kotlin coroutines and their usage in JavaFX applications and today I want to explore coroutines even further and talk about them in the context of actors.

Why not Threads?

A modern software system has to manage a lot of different tasks at the same time. User requests coming in via web interfaces, GUIs and other channels need to be handled while there is background work to be done. Traditionally this has been the domain of operation system threads that the application spawned and provided with work.

Photo by Héctor J. Rivas on UnsplashPhoto by Héctor J. Rivas

But as you may know, threading is error-prone and not easily done right when it comes to mutating state in an complex application (if you don’t you should read Why Threads Are A Bad Idea (for most purposes)). With coroutines, there is a lightweight and simple alternative to threads in Kotlin that follows the approach of Communicating Sequential Processes. It’s the same idea that inspired Go’s goroutines:

Do not communicate by sharing memory; instead, share memory by communicating. - Effective Go

Kotlin achieves that by providing the concept of a Channel, which basically is a Queue that uses suspending functions. Using coroutines and channels, we can build a system that encapsulate mutable state in a manner that do not need any locks and synchronization and instead leverage a protocol of messages to handle concurrent updates of that state.

The actor model

Photo by Avel Chuklanov on UnsplashPhoto by Avel Chuklanov

Such a model is called an actor. It is not a new concept, instead it has been around for years and is the underlying concept of Erlang and can also be used in Java and Scala using Akka. In How does keeing state in Elixir work? I built an actor in Elixir even though the concept is not officially called an ‘actor’ in the BEAM world, but gen_server instead.

Actors come in many sizes and they can do a wide variety of tasks, but they all share the same properties:

  • they keep internal state
  • they are running concurrently
  • their state can be manipulated through messages only
  • they receive messages in a channel that is called a ‘mail box’
  • they process messages sequentially

Don’t try this at home!

Ok, this might be a bit exaggerated, but there is a need to warn you. Precisely to warn you about the state of implementation for the actor() function. As of writing it is a very simple API to create an actor but this is about to change in future releases of kotlinx.coroutines.

basic counter example

So what does an actor in Kotlin look like? Let’s have a look.

First of all, we need to define messages we can send to the actor. In this simple example, the actor stores a counter that can be incremented by arbitrary integer values using Message.Increment and the current value can be requested by Message.GetValue.

To send data back to the requesting code, we use a CompletableDeferred here.

sealed class Message {
    class Increment(val value: Int) : Message()
    class GetValue(val deferred: CompletableDeferred<Int>) : Message()
}
Enter fullscreen mode Exit fullscreen mode

Next up, we define the actor named basicActor itself. It needs to be defined as an extension function to CoroutineScope as it will launch a coroutine to run in. It runs a simple for loop to get messages from the channel that will run until anyone closes the channel. The handling of the messages is rather straight forward.

fun CoroutineScope.basicActor() = actor<Message> {
    var counter = 0

    for (message in channel) {
        when(message) {
            is Message.Increment -> counter += message.value
            is Message.GetValue -> message.deferred.complete(counter)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That’s it! But to use it, you also have to define a client that sends messages to the actor. Here’s the code:

fun main() = runBlocking<Unit> {
    val channel = basicActor()

    channel.send(Message.Increment(1))
    channel.send(Message.Increment(2))

    val deferred = CompletableDeferred<Int>()

    channel.send(Message.GetValue(deferred))

    println(deferred.await()) // prints "3"

    channel.close()
}
Enter fullscreen mode Exit fullscreen mode

In main we define a CoroutineScope by using runBlocking() which is then used to launch the actor in and send messages. channel.close() will then cause the actor to complete. Without it, the actor will keep the program running infinitely, which may be a desired state in a typically server situation.

Conclusion

Actors are an easy concept that can help to remove the hassle of concurrent computing. They are still experimental in kotlinx.coroutines but it’s a first step in the direction and I am curious of the developments and as a long-time fan of Erlang and Elixir I appreciate actors and would happily welcome them as a stable component of Kotlin coroutines.

Top comments (0)