One of the more confusing things about Kotlin Native (henceforth 'KN') for new developers is the state and concurrency model. In general, Kotlin developers come from the JVM, and expect everything to be the same. It is not.
However, although different, the model is conceptually simple. With practice, getting your head around it is not too difficult.
This post series is part of the KMP Starter Kit available here: https://go.touchlab.co/kampdevto
Why?
Languages like Java, C++, Swift/Objc, let multiple threads access the same state in an unrestricted fashion. It is up to you, the developer, to avoid doing bad things. Concurrency issues are unlike other programming issues in that they are often very difficult to reproduce. You won't see them locally, but in production, at load, they emerge.
Just because your tests pass does not mean your code is OK.
Not all languages are designed this way. Most familiar to developers would be Javascript. You can achieve some level of concurrency with Worker
, but you can't reference and modify the same state at the same time. The language Rust is built for performance and safety. Concurrency management is designed into the language, so it is essentially like C++ minus the inherent risk of unrestricted shared state 1.
KN has rules around how state is shared between threads. These rules exist for KN but not Kotlin JVM (henceforth KJ). However, a critical goal of Kotlin Multiplatform is that the various versions of Kotlin remain source-compatible. Changing the language itself to enforce concurrency (like Rust), while in some ways appealing, would break support for JS and JVM. Thus, the new rules for KN are implemented and enforced in the runtime.
Two Rules
The new rules are conceptually simple.
1) Mutable state == 1 thread
If your state is mutable, only one thread can "see" it at a time. We'll explain what that means later, but assume all state is "mutable", and if you aren't doing any concurrency, KN will feel pretty much the same as anything else you've written in Kotlin (except if you use a top-level property or companion object
. See "Global State" in Part 2 for detail).
The goal of rule 1 is simple. If there's only 1 thread, you don't have concurrency issues. Technically this is referred to as "thread confinement". For native mobile and desktop UI developers, this should be familiar. You cannot change the UI from a background thread. KN has generalized that concept.
2) Immutable state == many threads
If state can't be changed, it can be shared between threads. This is also conceptually simple.
That's it. Two rules.
How that is implemented, and the implications, are obviously more involved, but the basic concepts underlying KN and concurrency are very simple.
Status
Just a quick sidebar on the status of KN and it's concurrency rules. The community has been growing over the past few years, and there is pressure to relax these rules to make the transition from KJ easier. That may continue over time, but not in a breaking way. That means, it's possible these rules will be relaxed a bit in 2020, but code written now will work. There is also pressure to keep these rules, and simply improve the onboarding experience. You could say I'm in that camp, hence this post. Anyway...
Code!!!
First, get the sample. To use the sample you'll need Intellij Community (or Ultimate) 2019.3 or higher.
Clone the sample repo:
Once cloned, open the sample project in Intellij.
1) Simple State
First we'll start with some basic state. Basic vars, mutating values. Not super interesting. Just showing how things work when you're not doing any concurrency.
Open the sample project in Intellij. Find SampleMacos.kt
. In the main function there will be code that is commented out. Look for 1) Simple State
and uncomment runSimpleState()
.
Over on the right of the IDE, find "Gradle", then in that window, find runDebugExecutableMacos
and double-click.
This is a very basic sample. It is demonstrating that state which is in a single thread is mutable as usual.
fun runSimpleState(){
val s = SimpleState()
s.increment()
s.increment()
s.report()
s.decrement()
s.report()
}
class SimpleState{
var count = 0
fun increment(){
count++
}
fun decrement(){
count--
}
fun report(){
println("My count $count")
}
}
This will print:
My count 2
My count 1
Moving on...
2) Frozen State
All state is mutable unless it is frozen. Frozen state is a new concept for Kotlin, although it exists in some other languages. In KN, there is a function freeze()
defined on all classes. When you call freeze()
, that object and everything it touches is frozen and immutable.
Once frozen, state can be shared between threads, but it cannot be changed.
Look for "2) Frozen State" in the SampleMacos.kt
. Uncomment freezeSomeState()
and run it (by "run it" from now on I mean run runDebugExecutableMacos
again).
fun freezeSomeState(){
val sd = SomeData("Hello 🐶", 22)
sd.freeze()
println("Am I frozen? ${sd.isFrozen}")
}
data class SomeData(val s:String, val i:Int)
You should see
Am I frozen? true
Understand that your state is being changed at runtime. There is a flag on every object in KN that says if it is frozen or not, and for sd
we have just flipped it to true.
Back in SampleMacos.kt
, comment out any other uncommented method, and uncomment failChanges()
. Run again.
This will fail with an exception. The output console in Intellij can be a little confusing to navigate. Make sure you click the top level to see the full output.
The code is as follows
fun failChanges(){
val smd = SomeMutableData(3)
smd.i++
println("smd: $smd")
smd.freeze()
smd.i++
println("smd: $smd") //We won't actually get here
}
data class SomeMutableData(var i:Int)
The output looks like this
smd: SomeMutableData(i=4)
Uncaught Kotlin exception: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen sample.SomeMutableData@8b40c4b8
at 0 KNConcurrencySamples.kexe (yada yada)
at 1 KNConcurrencySamples.kexe (yada yada)
(etc)
The important points. Before we freeze the object, you can change the var
value. After we freeze, if you try to change the value, it'll throw an exception. InvalidMutabilityException
, to be precise.
InvalidMutabilityException
is your new friend. You will probably not feel that way at first, but it is.
When you see InvalidMutabilityException
, it means you are attempting to change some state that has been frozen. You probably did not want this state to be frozen, so your job will be to figure out why it was frozen. Remember, when you freeze something, everything it touches is frozen. We'll talk about this in detail later. All I can say from experience is, at first this will be somewhat confusing. Pretty soon it won't be. Just understand the system and the tools you can use to debug.
End
That's it for part one. We have installed the IDE, and run some basic sample functions. We have not written any concurrent code yet. Step one is simply understanding the basics.
These posts are intended to be a functional intro to concurrency on KN. We're going to skip over a lot and just present what you're likely to use day to day. If you'd like a deeper dive, check out Stranger Threads and my Kotlinconf talk.
-
Yes, I'm sure Rust is other things too, but in this context... ↩
Top comments (0)