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.
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
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()
}
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)
}
}
}
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()
}
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.
Oldest comments (0)