Kotlin Native. Multithreading with Coroutines
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:
Next we need to setup scopes and contexts of coroutines to work with main and background threads:
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:
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:
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:
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:
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:
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.