Slide 1

Slide 1 text

A short dive into the world of actors in Kotlin Mastering concurrency with coroutines

Slide 2

Slide 2 text

Coroutines are cool but.. How to gurantee reading consistent states all the time? Should I use synchronized blocks? How are errors handled and propagated? Is there an equivalent to thread local with coroutines? .. or monitors?

Slide 3

Slide 3 text

About us Nils Ernsting Holisticon AG is a management and IT consultancy based in Hamburg. With a holistic consulting approach, we support our customers in their development projects on a technical, tactical and strategic level.

Slide 4

Slide 4 text

Actor Model Actors share nothing!

Slide 5

Slide 5 text

Actor ■ Isolated lightweight processes (Kotlin coroutines) ■ Asynchronous and non-blocking ■ Can only access internal state ■ Share state by sending messages ■ Each actor has an own mailbox (channel) ■ Identified via address

Slide 6

Slide 6 text

How an actor acts ■ Processes one message at a time ■ In response to a message, an actor can concurrently.. ¨ .. send a finite number of messages to other actors ¨ .. create a finite number of new actors ¨ .. designate the behaviour to be used for the next message it receives ■ No sequence assumed in which the above actions are executed

Slide 7

Slide 7 text

Message passing ■ Messages are not necessarily delivered instantly ■ No sequence can be assumed on the receivers side ■ .. but it can be assumed that a message is delivered ■ Features unbound indeterminancy

Slide 8

Slide 8 text

Advantages ■ Easier to avoid ¨ Race Conditions ¨ Deadlocks ¨ Starvation ¨ Live Locks ■ Variable Topology ■ Easier to consider scalability and distribution

Slide 9

Slide 9 text

And what about Kotlin?

Slide 10

Slide 10 text

Channel Signature in Kotlin public interface Channel : SendChannel, ReceiveChannel public interface SendChannel { public suspend fun send(element: E) public val onSend: SelectClause2> public fun offer(element: E): Boolean } public interface ReceiveChannel { public suspend fun receive(): E public val onReceive: SelectClause1 public fun poll(): E? }

Slide 11

Slide 11 text

Channel Buffer Types ■ 4 different buffer types ¨ Rendevous (Unbuffered) ¨ Conflated ¨ Buffered ¨ Unlimited ■ Backpressure through limited buffer

Slide 12

Slide 12 text

Channel Example val channel = Channel() launch { for (x in 1..5) channel.send(x * x) channel.close() } for (y in channel) println(y) println("Done!")

Slide 13

Slide 13 text

Actors in Kotlin ■ kotlinx.coroutines provides an experimental API to build an Actor ■ Returns a SendChannel public fun CoroutineScope.actor( context: CoroutineContext = EmptyCoroutineContext, capacity: Int = 0, start: CoroutineStart = CoroutineStart.DEFAULT, onCompletion: CompletionHandler? = null, block: suspend ActorScope.() -> Unit ): SendChannel

Slide 14

Slide 14 text

Actors in action Goal: Build our own crypto currency .. implementing actor model .. but without the crypto stuff ▸ Simple bank account example

Slide 15

Slide 15 text

Banking Account: Messages and State sealed class BankingMessage data class Deposit(val amount: Int) : BankingMessage() data class Withdrawal(val response: SendChannel, val amount: Int) : BankingMessage() data class Balance(val response: SendChannel) : BankingMessage() data class BankingAccountState(val name: String, val balance: Int = 0)

Slide 16

Slide 16 text

Banking Account: Actor @ObsoleteCoroutinesApi fun bankAccount(name: String) = GlobalScope.actor { var state = BankingAccountState(name) for (msg in channel) when (msg) { is Withdrawal -> if (msg.amount > state.balance) { msg.response.offer(false) } else { msg.response.offer(true) state = state.copy(balance = state.balance - msg.amount) } .. } }

Slide 17

Slide 17 text

Banking Account: Access Methods typealias BankAccount = SendChannel suspend fun BankAccount.deposit(amount:Int) = this.send(Deposit(amount)) suspend fun BankAccount.withdrawal(amount:Int) = Channel() .also { this.send(Withdrawal(it, amount)) } suspend fun BankAccount.balance() = Channel() .also { this.send(Balance(it)) }

Slide 18

Slide 18 text

Banking Account: Access Methods typealias BankAccount = SendChannel suspend fun BankAccount.deposit(amount:Int) = this.send(Deposit(amount)) suspend fun BankAccount.withdrawal(amount:Int) = Channel(capacity=1) .also { this.send(Withdrawal(it, amount)) } suspend fun BankAccount.balance() = Channel(capacity=1) .also { this.send(Balance(it)) }

Slide 19

Slide 19 text

Banking Account: Usage val ron = bankAccount("Ron") val amelie = bankAccount("Amelie") ron.deposit(50) amelie.deposit(100) val success = ron.withdrawal(20).receive() println(ron.balance().receive())

Slide 20

Slide 20 text

How to transfer money between two actors?

Slide 21

Slide 21 text

Banking Account: Money Transfer @ObsoleteCoroutinesApi fun transfer(sender: BankAccount, receiver: BankAccount, amount: Int) = Channel().also { GlobalScope.actor { val result = sender.withdrawal(amount).receive() if (result) { receiver.deposit(amount) } it.send(result) } } val success = transfer(sender = ron, receiver = amelie, amount = 50) .receive()

Slide 22

Slide 22 text

Summary ■ Shared state is evil in a concurrent environment ■ Actor Model fits well in these situations ■ Higher Memory consumption ■ Errors are handled on a higher abstraction level

Slide 23

Slide 23 text

Summary ■ Actor Model is not well represented in Kotlin (kotlinx.coroutines) ■ Backpressure in Kotlin can lead to known concurrency issues [..] we do not plan to turn kotlinx.coroutines into an actor-based framework [..]. In terms of kotlinx.coroutines actor is just a code organization pattern -- a coroutine plus an incomming channel. - Roman Elizarov, JetBrains

Slide 24

Slide 24 text

Questions? Example can be found here https://gist.github.com/nernsting/ebf5e5c11cdd07ec797e00955ba6dac6