Kotlinx.coroutines: Correct way to implement MainDispatcher and BackgroundDispatcher in iOS

Created on 31 Mar 2020  路  3Comments  路  Source: Kotlin/kotlinx.coroutines

Platform: iOS, org.jetbrains.kotlinx:kotlinx-coroutines-core-iosarm64
Version: 1.3.5-native-mt (so multithreading should be supported in iOS?)

I simply want to operate in the UI thread by default, and do some actions, e.g. network calls, in the background thread, and then return some data back to the iOS app in the UI thread. Trivially.

For now, I have:

actual object DispatchersProvider {
    actual val dispatcherDefault: CoroutineDispatcher = IosBackgroundDispatcher
    actual val dispatcherUi: CoroutineDispatcher = IosMainDispatcher
}

private object IosMainDispatcher : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        // should I freeze `dispatch_get_main_queue`? or `block` before `run`?
        dispatch_async(dispatch_get_main_queue(), block::run)
    }
}

private object IosBackgroundDispatcher : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND.convert(), 0.convert()), block::run)
    }
}

It seems like ok with the main dispatcher, but when I'm trying to do something like this:

class IssueReproduce: CoroutineScope {
    private val dispatcher = DispatchersProvider.dispatcherUi
    private val job = SuperviserJob()
    private val someApi = SomeApi()

    override val coroutineContext: CoroutineContext
        get() = dispatcher + job

    fun performApiCall(callback: (SomeDataClass) -> Unit) {
        launch(coroutineContext) {
            val response = withContext(DispatchersProvider.dispatcherDefault) {
                someApi.someMethod()
            }

            callback(response)
        }
    }
}

/* ... */

class SomeApi {
    private val client: HttpClient = /* ... */

    suspend fun someMethod(): SomeDataClass {
        return client.get { /* ... */ }
    }
}

then I get errors like this:

Uncaught Kotlin exception: kotlin.native.IncorrectDereferenceException: illegal attempt to access non-shared com.example.mobile.IosBackgroundDispatcher.$run$FUNCTION_REFERENCE$20@81b41e88 from other thread
        at 0   ExampleAppCommon                    0x0000000103846310 kfun:kotlin.Throwable.<init>(kotlin.String?)kotlin.Throwable + 92
        at 1   ExampleAppCommon                    0x000000010383f170 kfun:kotlin.Exception.<init>(kotlin.String?)kotlin.Exception + 88
        at 2   ExampleAppCommon                    0x000000010383ec34 kfun:kotlin.RuntimeException.<init>(kotlin.String?)kotlin.RuntimeException + 88
        at 3   ExampleAppCommon                    0x000000010386da5c kfun:kotlin.native.IncorrectDereferenceException.<init>(kotlin.String)kotlin.native.IncorrectDereferenceException + 88
        at 4   ExampleAppCommon                    0x0000000103871284 ThrowIllegalObjectSharingException + 496
        at 5   ExampleAppCommon                    0x0000000103c60478 _ZNK16KRefSharedHolder3refEv + 100
        at 6   ExampleAppCommon                    0x0000000103c3adb8 _ZL39Kotlin_Interop_unwrapKotlinObjectHolderP11objc_object + 48
        at 7   ExampleAppCommon                    0x0000000103ba145c _4e6967687453746f7279436f6d6d6f6e_knbridge4 + 120
        at 8   libdispatch.dylib                   0x0000000104429d10 _dispatch_call_block_and_release + 32
        at 9   libdispatch.dylib                   0x000000010442b18c _dispatch_client_callout + 20
        at 10  libdispatch.dylib                   0x000000010443d8b8 _dispatch_root_queue_drain + 908
        at 11  libdispatch.dylib                   0x000000010443e030 _dispatch_worker_thread2 + 140
        at 12  libsystem_pthread.dylib             0x00000001b45c36d8 _pthread_wqthread + 216
        at 13  libsystem_pthread.dylib             0x00000001b45c99c8 start_wqthread + 8
(lldb) 

If I change dispatcherDefault to the Dispatchers.Default, then I get errors like this:

mutation attempt of frozen io.ktor.client.request.HttpRequestPipeline@83d78848, e=kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen io.ktor.client.request.HttpRequestPipeline@83d78848
native question

Most helpful comment

Yes, that's a ktor issue. You cannot share HttpClient between threads in Kotlin/Native. The recommended way is to create HttpClient in your Main dispatcher and it will take care of performing all the actual HTTP work in the background itself. You don't have to worry about it.

All 3 comments

You don't need to implement anything with -native-mt version:

  • For main thread use Dispatchers.Main
  • For background thread use Dispatchers.Default

In particular, you cannot use dispatch_get_global_queue(QOS_CLASS_BACKGROUND) to dispatch coroutines in Kotlin/Native, because this background queue is not bound to any particular thread and Kotlin/Native does not allow moving mutable objects (including coroutines) between threads.

Does it help?

@elizarov
Okay, I changed dispatchers to this:

actual object DispatchersProvider {
    actual val dispatcherDefault: CoroutineDispatcher = Dispatchers.Default // IosBackgroundDispatcher
    actual val dispatcherUi: CoroutineDispatcher = Dispatchers.Main // IosMainDispatcher
}

Then I get this error:

Caused by: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen io.ktor.client.request.HttpRequestPipeline@81ed68c8
        at 0   ExampleAppCommon                    0x00000001056376e8 kfun:kotlin.Throwable.<init>(kotlin.String?)kotlin.Throwable + 92
        at 1   ExampleAppCommon                    0x0000000105630548 kfun:kotlin.Exception.<init>(kotlin.String?)kotlin.Exception + 88
        at 2   ExampleAppCommon                    0x000000010563000c kfun:kotlin.RuntimeException.<init>(kotlin.String?)kotlin.RuntimeException + 88
        at 3   ExampleAppCommon                    0x0000000105660fe8 kfun:kotlin.native.concurrent.InvalidMutabilityException.<init>(kotlin.String)kotlin.native.concurrent.InvalidMutabilityException + 88
        at 4   ExampleAppCommon                    0x000000010566242c ThrowInvalidMutabilityException + 468
        at 5   ExampleAppCommon                    0x0000000105a4ef68 MutationCheck + 132
        at 6   ExampleAppCommon                    0x000000010587b94c kfun:io.ktor.util.pipeline.Pipeline.<set-interceptors>#internal + 104
        at 7   ExampleAppCommon                    0x000000010587be20 kfun:io.ktor.util.pipeline.Pipeline.notSharedInterceptorsList#internal + 88
        at 8   ExampleAppCommon                    0x000000010587a450 kfun:io.ktor.util.pipeline.Pipeline.cacheInterceptors#internal + 504
        at 9   ExampleAppCommon                    0x000000010587bcdc kfun:io.ktor.util.pipeline.Pipeline.sharedInterceptorsList#internal + 292
        at 10  ExampleAppCommon                    0x00000001058771d4 kfun:io.ktor.util.pipeline.Pipeline.createContext#internal + 200
        at 11  ExampleAppCommon                    0x000000010587703c kfun:io.ktor.util.pipeline.Pipeline.execute(TContext;TSubject)TSubject + 212
        at 12  ExampleAppCommon                    0x00000001058b6008 kfun:io.ktor.client.HttpClient.$executeCOROUTINE$16.invokeSuspend(kotlin.Result<kotlin.Any?>)kotlin.Any? + 504
        at 13  ExampleAppCommon                    0x00000001058b62d4 kfun:io.ktor.client.HttpClient.execute(io.ktor.client.request.HttpRequestBuilder)io.ktor.client.call.HttpClientCall + 256
        at 14  ExampleAppCommon                    0x00000001058ebb38 kfun:io.ktor.client.statement.HttpStatement.$executeUnsafeCOROUTINE$50.invokeSuspend(kotlin.Result<kotlin.Any?>)kotlin.Any? + 712
        at 15  ExampleAppCommon                    0x00000001058ebdec kfun:io.ktor.client.statement.HttpStatement.executeUnsafe$ktor-client-core()io.ktor.client.statement.HttpResponse + 212
        at 16  ExampleAppCommon                    0x0000000105974e74 kfun:com.example.mobile.data.api.SomeApi.$someMethodCOROUTINE$3.invokeSuspend(kotlin.Result<kotlin.Any?>)kotlin.Any? + 4560
        at 17  ExampleAppCommon                    0x0000000105975c14 kfun:com.example.mobile.data.api.SomeApi.someMethod(kotlin.Int;kotlin.Int;kotlin.String)com.example.mobile.data.model.dto.ContentResponse<com.example.mobile.data.model.dto.SomeDataClass> + 308
        at 18  ExampleAppCommon                    0x000000010598fdd8 kfun:com.example.mobile.presentation.IssueReproduce.$performApiCall$lambda-1$lambda-0COROUTINE$6.invokeSuspend#internal + 520
        at 19  ExampleAppCommon                    0x0000000105655b8c kfun:kotlin.coroutines.native.internal.BaseContinuationImpl.resumeWith(kotlin.Result<kotlin.Any?>) + 524
        at 20  ExampleAppCommon                    0x000000010579c304 kfun:kotlinx.coroutines.DispatchedTask.run() + 2284
        at 21  ExampleAppCommon                    0x000000010575ccec kfun:kotlinx.coroutines.EventLoopImplBase.processNextEvent()kotlin.Long + 620
        at 22  ExampleAppCommon                    0x00000001057b1f7c kfun:kotlinx.coroutines.runEventLoop$kotlinx-coroutines-core(kotlinx.coroutines.EventLoop?;kotlin.Function0<kotlin.Boolean>) + 744
        at 23  ExampleAppCommon                    0x00000001057b88fc kfun:kotlinx.coroutines.WorkerCoroutineDispatcherImpl.start$lambda-0#internal + 312
        at 24  ExampleAppCommon                    0x00000001057b8ac4 kfun:kotlinx.coroutines.WorkerCoroutineDispatcherImpl.$start$lambda-0$FUNCTION_REFERENCE$135.invoke#internal + 64
        at 25  ExampleAppCommon                    0x00000001057b8b24 kfun:kotlinx.coroutines.WorkerCoroutineDispatcherImpl.$start$lambda-0$FUNCTION_REFERENCE$135.$<bridge-UNN>invoke()#internal + 64
        at 26  ExampleAppCommon                    0x00000001056620e0 WorkerLaunchpad + 188
        at 27  ExampleAppCommon                    0x0000000105a64034 _ZN6Worker19processQueueElementEb + 1924
        at 28  ExampleAppCommon                    0x0000000105a652ec _ZN12_GLOBAL__N_113workerRoutineEPv + 72
        at 29  libsystem_pthread.dylib             0x00000001b45c18fc _pthread_start + 168
(lldb) 

Is this related to ktor, maybe? The logic is simple and shown above: I have HttpClient() of ktor-client-ios.

Yes, that's a ktor issue. You cannot share HttpClient between threads in Kotlin/Native. The recommended way is to create HttpClient in your Main dispatcher and it will take care of performing all the actual HTTP work in the background itself. You don't have to worry about it.

Was this page helpful?
0 / 5 - 0 ratings