Concurrency happens to be a very important topic today in programming. Unfortunately, you encounter a different state and concurrency model for iOS once you extend your development experience from Android to Kotlin Multiplatform Mobile (KMM). Kotlin/Native model kicks in at this point.
Kotlin/Native is a technology for compiling Kotlin code to native binaries, which can run without a virtual machine.
With Kotlin/Native, Kotlin code is able to compile to native binaries for macOS, iOS, Windows, etc.
State Sharing
Unlike the way languages like Java, C++ and Swift let multiple threads access the same state in an unrestricted way, Kotlin/Native introduces rules for sharing states between threads. Having mutable memory available to multiple threads at the same time, if unrestricted, is known to be risky and prone to error, that is why these rules exist to prevent unsafe shared access to multiple states. These rules are simple and straightforward:
Rule 1: Mutable state == 1 thread
Just as it reads, only one thread can access a mutable state at a time. Unlike what you may think, any regular class state that you would normally use in Kotlin is considered by the Kotlin /Native runtime as mutable. Meanwhile, if you have only one thread (say main thread), you won’t have concurrency issues.
Rule 2: Immutable state == many threads
Kinda like the opposite of the first rule, if a state can’t be changed, multiple threads can safely access it. Meanwhile, in Kotlin/Native, immutable doesn’t imply the use of val
. It actually means frozen state.
Immutable and Frozen State
Kotlin/Native defines a new runtime state called frozen. Any object can be frozen and if frozen:
- You cannot change (mutate) any part of its state. Any attempt to do so will result in an
InvalidMutabilityException
at runtime. A frozen object instance is 100% immutable. - Everything it references is frozen. This basically means if an object is frozen, the whole graph is also frozen and is safe to be shared.
The Kotlin/Native runtime adds an extension function freeze()
to all classes and calling this function freezes an object and everything referenced by the object.
data class SomeData(val name: String, var age: Int) //... val sd = SomeData("Joe", 35) sd.freeze()
A few things to note about freeze() though:
freeze()
is a one-way operation. You can’t unfreeze an object.freeze()
is not available in shared kotlin code, but several libraries provide expect and actual declarations for using it in shared code. For example, Ktor provides themakeShared()
function which calls thefreeze()
behind the hood.
Global objects and properties
Kotlin global state has some special conditions that enables it to conform to Kotlin/Native’s state rules. These conditions either freeze the state or make it visible only to a single thread.
Global object instances are frozen by default, all threads can access them, but they are immutable, so any attempt to mutate it would violate Rule 1 above and throw an exception.
object SomeState{ var count = 0 fun add(){ count++ // This will throw an exception } }
You can make a global object thread local, which allows it to be mutable and give each thread a copy of its state. This is done by annotating the object with @ThreadLocal
@ThreadLocal object SomeState{ var count = 0 fun add(){ count++ // Works? } }
If different threads read count
, they will get different values, because each thread gets its own copy.
Global properties are only available on the main thread, but they are mutable. An attempt to access them from other threads will throw an exception.
You can annotate them with :
@SharedImmutable
, which will make them globally available to multiple threads but frozen.@ThreadLocal
, which will give each thread its own mutable copy.
Implementing concurrency
So, I have described what the concurrency model looks like in Kotlin/Native. But the question is, how is concurrency implemented in KMM? With the standard Kotlin coroutines, you can write multithreaded code that runs in parallel.
Coroutines are light-weight threads that allow you to write asynchronous non-blocking code.
The current version of Kotlinx coroutines (1.4.2), which can be used for iOS, supports usage only in a single thread.
Virtually all the sample KMM projects you would find on Github today uses this version of Kotlinx coroutines. You see an implementation similar to this:
class MainScope : CoroutineScope { override val coroutineContext: CoroutineContext = NsQueueDispatcher(dispatch_get_main_queue()) } internal class NsQueueDispatcher( private val dispatchQueue: dispatch_queue_t ) : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatchQueue) { block.run() } } }
This implementation uses just one scope which provides a CoroutineContext that makes use of the main queue
from the Kotlin/Native platform
library. This MainScope
is used for both main and background thread in iOS. This means the main thread is used to run both the UI processes and the background processes. Android, on the other hand, can perfectly switch threads using the Dispatchers.Main
, Dispatchers.io
and Dispatchers.default
.
With this implementation, even though you are somewhat implementing concurrency, iOS here doesn’t enjoy the actual benefits of being able to switch between threads, so performance is eventually affected.
So what’s the solution to this?
Multithreaded coroutines
There is another version of Kotlinx coroutines that provide support for multiple threads. Even though it is made a separate branch of the library for some reasons listed here, it is, however, safe to use this multithreaded version in production.
The current version for Kotlin 1.4.30 is 1.4.2-native-mt
and it is to be added as a dependency for the commonMain
source set in build.gradle.kts
as described below:
commonMain { dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2-native-mt" } }
With this version of coroutines, you get to use Dispatchers.Main
for your main thread and Dispatchers.Default
for background thread in iOS. For more information about this, you can check out this doc provided by the Kotlin Coroutines team.
class MainScope : CoroutineScope { override val coroutineContext: CoroutineContext = Dispatchers.Main } class BackgroundScope() : CoroutineScope { override val coroutineContext: CoroutineContext = Dispatchers.Default }
Above is an example implementation that provides two scopes (you can name it anything you want) that provides CoroutineContext that is used to switch between threads in iOS.
One thing to note is that, with a successful thread switch as described above, the Kotlin/Native rules kick in and have to be followed or you might run into exceptions at runtime on iOS, which are mostly descriptive and can be fixed easily.
Work Around
For some reasons, it will not always be feasible to constrain a mutable state to a single thread. There are various techniques that help you work around these restrictions, each with their own pros and cons. I will discuss two of them.
Atomics
Kotlin/Native provides a set of Atomic classes that can be frozen while still supporting changes to the value they contain. These classes implement a special-case handling of states in the Kotlin/Native runtime. This means that you can change values inside a frozen state. Some of them are AtomicInt
, AtomicLong
, AtomicReference
. Values wrapped with these classes can be read and changed from multiple threads.
object AtomicDataCounter { val count = AtomicInt(3) fun addOne() { count.increment() } }
However, accessing and changing values in an Atomics is very costly performance-wise. Also, there are potential issues with memory leaks.
You can read more about atomics here.
Low-level capabilities
Kotlin/Native runs on top of C++ and provides interop with C and Objective-C. For iOS, you can pass lambda arguments into your shared code from Swift. All of this swift native code runs outside of the Kotlin/Native state restrictions. That means that you can implement a concurrent mutable state in a native language and have Kotlin/Native talk to it.
I have found this to be the best solution and it has helped in situations where you want to keep data in memory and want them to be mutable and accessible from the background thread.
// Shared Kotlin Code class NetworkLocalDataSource(private val inMemoryStorage: InMemoryStorage) { fun saveNetwork(network: NetworkModel) { inMemoryStorage.saveNetwork(network) } fun getNetwork() = inMemoryStorage.getNetwork() } // Swift class InMemoryStorageImpl: InMemoryStorage { private var network: NetworkModel = NetworkModel() func saveNetwork(network: NetworkModel) { self.network = network } func getNetwork() -> NetworkModel { return network } }
Above is an example of Kotlin/Native talking to swift native code, through which it saves and gets network
. Even though this process of saving and getting is done in the background thread, the variable network
in InMemoryStorageImpl
is safe from Kotlin/Native state restrictions.
Alternatives to Kotlin Coroutines
There are a few other libraries that provide multithreading support apart from Kotlin Coroutines. There is CoroutineWorker, a library published by AutoDesk. There is also Reaktive, an Rx-like library that implements Reactive extensions for Kotlin Multiplatform. You can find more of these alternatives here.
As you can see, it’s not very difficult to provide multithreading support in any KMM project. You just have to understand and follow the rules and you would be good. If you find it not clear the first time, you can always go over it again. Also, do well to try it out, it helps.
If you have any questions, feel free to reach out. Cheers!