Kotlin Native. Multithreading with Coroutines

Usetech
4 min readNov 11, 2022

--

The main idea of Kotlin Multiplatform, as well as other cross-platform SDKs, is to optimize development by writing code once and share it between different platforms. However, there are some nuances that should be figured out and solved according to the platform specifics. One such moment is the concurrency. KMM SDK uses the specific for every native platform version of Kotlin: Kotlin/JVM, Kotlin/JS, or Kotlin/Native. Kotlin Native is really different from Kotlin JVM because it depends on the specifics of the iOS platform. Most default solutions that work with JVM are not suitable for Kotlin Native at all. In this story we will discuss about the basic approach to deal with concurrency for iOS and Kotlin Native in Kotlin Multiplatform.

Kotlin Multiplatform provides common way to implement the multithreading. It uses Kotlin, so we can use Coroutines for all our targets:

sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutines_version}")
}
}
val androidMain by getting

val iosMain by getting
}

Next we need to setup scopes and contexts of coroutines to work with main and background threads:


expect val defaultDispatcher: CoroutineContext

expect val uiDispatcher: CoroutineContext

Because of platform specific code we need to use expect/actual mechanism to setup correct versions for every platform we use.
For both Android and iOS we can use Default and Main dispatchers:

actual val uiDispatcher: CoroutineContext
get() = Dispatchers.Main

actual val defaultDispatcher: CoroutineContext
get() = Dispatchers.Default

There are no problem with concurrency in JVM and Android, so we will focus just on Kotlin Native. And there we will face some nuances.
If we use provided by default dispatchers and share some objects between main and background threads, we can get FreezingException:

In Kotlin Native we can share between threads only immutable objects. And to make an object or block of code immutable, we should use freeze() command. To incapsulate and hide all the work with freezing under the hood, we can create our own Coroutine Dispatchers:

actual val defaultDispatcher: CoroutineContext
get() = IODispatcher

actual val uiDispatcher: CoroutineContext
get() = MainDispatcher

private object MainDispatcher: CoroutineDispatcher(){
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_main_queue()) {
try {
block.run().freeze()
}catch (err: Throwable) {
throw err
}
}
}
}
//Background
private object IODispatcher: CoroutineDispatcher(){
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(),
0.toULong())) {
try {
block.run().freeze()
}catch (err: Throwable) {
throw err
}
}
}

It is absolutely correct to use MainDispatcher to work with main queue. But dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.toLong(), 0.toULong()) cannot be used for Global queue, because in Kotlin Native it is not bound to any particular thread.
We can use MainDispatcher for both main and background thread:

actual val defaultDispatcher: CoroutineContext
get() = MainDispatcher

actual val uiDispatcher: CoroutineContext
get() = MainDispatcher


@ThreadLocal
private object MainDispatcher: CoroutineDispatcher(){
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatch_get_main_queue()) {
try {
block.run().freeze()
}catch (err: Throwable) {
throw err
}
}
}

ThreadLocal annotation is used to make a singleton object shareable between threads.

Using MainDispatcher looks really strange. But it is ok, when we use it with such library as Ktor (for e.g). Because Ktor already has an asynchronous mechanism implemented under its hood.

But what if we don’t want to use Ktor? How can we deal with background threads in this case?
We can try to use Global Scope. But it is not recommended, because of potential leaks and lack of control under all coroutines launched in this scope:

//Not recommended
for i in 0..10 {
GlobalScope.launch {
work(i)
}
}

//Workaround
val jobs = mutableListOf<Job>()
for i in 0..10 {
jobs += GlobalScope.launch {
work(i)
}
}
jobs.forEach{ it.join() }

We can use simple workaround to manage all our coroutines in Global scope. But it still be not safe.

We can run our custom made dispatcher in other scope:


//defaultDispatcher uses MainDispatcher

val myJob = SupervisorJob()
val myScope: CoroutineScope = CoroutineScope(defaultDispatcher + myJob)

And it will work.
Also we can use special native-mt versions of coroutine library that allow us to use multithreaded coroutines, such as 1.5.2-native-mt. Because the main version of kotlinx.coroutines is a single-threaded one, libraries will almost certainly rely on this version. We can face InvalidMutabilityException in this case. Another problem is memory leaks while using multithreaded coroutines.

Well, it seems to be really tricky to use Coroutines in Kotlin Native. How to deal with concurrency without Coroutines, we will discuss on the next story.

--

--

Usetech

Usetech — Innovative AI solutions for your business