Kotlin / kotlinx.coroutines

Library support for Kotlin coroutines
Apache License 2.0
12.75k stars 1.82k forks source link

Default dispatcher and UI dispatcher support for iOS #470

Closed pkliang closed 2 years ago

pkliang commented 5 years ago

Hi, I am using 'org.jetbrains.kotlinx:kotlinx-coroutines-core-native:0.24.0' with outputKinds = [FRAMEWORK] to generate framework targeting ios_x64

I wrote some uint tests for the code which contains launch call and ran tests with Gradle without any problem, but I got this error when ran the code in iOS emulator.

There is no event loop. Use runBlocking { ... } to start one

Do we now have default dispatcher and UI dispatcher support for iOS?

qwwdfsad commented 5 years ago

We will have it with #462 Current default dispatcher was chosen as the simplest MVP and it's not something we want to stick with. Without #462 it's too error-prone to introduce "special" dispatcher for iOS, it will be constant source of confusing bugs in current API.

brettwillis commented 5 years ago

In my case, the current default dispatcher is not too much of a concern, because asynchronous operations are handled by other native libraries with callbacks on the main thread, and there are no intense computations inside coroutines, so it's fine for all coroutines to operate on the main thread.

However I can't use runBlocking to create an event loop in the context of a full iOS app. I tried below, but it obviously doesn't work because UIApplicationMain(...) never yields to runBlocking's event loop.

// The app's main.swift entrypoint (simplified)
import UIKit
import KotlinFramework

KotlinFramework.runBlocking {
    UIApplicationMain(
        CommandLine.argc,
        UnsafeMutableRawPointer(CommandLine.unsafeArgv).bindMemory(to: UnsafeMutablePointer<Int8>.self, capacity: Int(CommandLine.argc)),
        nil,
        NSStringFromClass(AppDelegate.self))
}

I'm looking into implementing my own iOS main event loop inside runBlocking instead of using UIApplicationMain(...), but so far it doesn't look straightforward at all...

The alternative could be to implement a CoroutineDispatcher as a wrapper around iOS's main NSRunLoop (for example). Is this what you refer to as being too bug-prone?

Is there a way that I can get single-threaded coroutines running in a full app while we wait for #462?

brettwillis commented 5 years ago

Well after giving that a go, it seems to work just fine as a temporary solution:

object MainLoopDispatcher: CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        NSRunLoop.mainRunLoop().performBlock {
            block.run()
        }
    }
}
kamerok commented 5 years ago

@brettwillis solution works as a charm. Make sure you removed delay() calls from your coroutines if you still get the error. Spent some time trying to figure that out

brettwillis commented 5 years ago

@kamerok , below is the implementation I'm currently using. Updated for coroutines 1.0 and implemented the delay parts, so you don't have to remove delay() calls.

@UseExperimental(InternalCoroutinesApi::class)
private object MainLoopDispatcher: CoroutineDispatcher(), Delay {

    override fun dispatch(context: CoroutineContext, block: Runnable) {
        dispatch_async(dispatch_get_main_queue()) {
            try {
                block.run()
            } catch (err: Throwable) {
                logError("UNCAUGHT", err.message ?: "", err)
                throw err
            }
        }
    }

    @InternalCoroutinesApi
    override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), dispatch_get_main_queue()) {
            try {
                with(continuation) {
                    resumeUndispatched(Unit)
                }
            } catch (err: Throwable) {
                logError("UNCAUGHT", err.message ?: "", err)
                throw err
            }
        }
    }

    @InternalCoroutinesApi
    override fun invokeOnTimeout(timeMillis: Long, block: Runnable): DisposableHandle {
        val handle = object : DisposableHandle {
            var disposed = false
                private set

            override fun dispose() {
                disposed = true
            }
        }
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, timeMillis * 1_000_000), dispatch_get_main_queue()) {
            try {
                if (!handle.disposed) {
                    block.run()
                }
            } catch (err: Throwable) {
                logError("UNCAUGHT", err.message ?: "", err)
                throw err
            }
        }

        return handle
    }

}
ildarsharafutdinov commented 5 years ago

hi guys,

So a custom CoroutineDispatcher is needed in order to run main-thread-bound coroutines on iOS. Something like @brettwillis mentioned above.

@qwwdfsad , @elizarov , is that correct?

qwwdfsad commented 5 years ago

Hi, yes, it looks correct on the first glace (note that I haven't tested this code). try/catch blocks are not really necessary because coroutine machinery should catch and report all exceptions by itself.

sschilli commented 4 years ago

Well after giving that a go, it seems to work just fine as a temporary solution:

object MainLoopDispatcher: CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        NSRunLoop.mainRunLoop().performBlock {
            block.run()
        }
    }
}

Is there an example usage of this? Where do I use the MainLoopDispatcher?

francos commented 4 years ago

I'm having this same issue, as @sschilli mentioned, is there an example on how to use the MainLoopDispatcher?

chris-hatton commented 4 years ago

@FrancoSabadini @sschilli There's a working example in my (still evolving) Kotlin Multi-platform Template. In short; one has to combine a Dispatcher with a root Job which forms a CoroutineScope - from which you can launch child Jobs.

The template defines three such Coroutine scopes: for UI, Process and Network (this is not intrinsic to co-routines, just my own 'starting point' for handling concurrency in Application projects).

In the JVM/Android target these are appropriately designated to the UI thread, a thread-pool and a virtually limitless thread creator.

For the iOS target, all three currently have to be designated to a dispatcher using iOS's main thread, due to this Kotlin/Native limitation.

Abuse of the main thread has its pitfalls, but this is a working solution for many kinds of application, for now.

francos commented 4 years ago

Thanks @chris-hatton, I figured out how to do it a couple of days back but that project is very useful! Thanks for sharing!

sschilli commented 4 years ago

@kamerok , below is the implementation I'm currently using. Updated for coroutines 1.0 and implemented the delay parts, so you don't have to remove delay() calls.

@brettwillis I've tried using this implementation to launch on macOS but it isn't working. I am doing the following:

@Test
fun `test launch`() {
    val scope = CoroutineScope(MainLoopDispatcher)
    scope.launch {
        println("hello world")
    }

    runBlocking {
        delay(5000) // wait to allow job to execute
    }
}

However, the block never gets executed (I never see "hello world" printed). Is there anything I am missing? I know MainLoopDispatcher.dispatch gets called but it just seems to never execute the block that is passed to it.

ennioma commented 3 years ago

I guess that using the version 1.4.2-native-mt we may stop using a custom Coroutine Dispatcher, just using the Main, is that right?

qwwdfsad commented 2 years ago

Fixed with 1.6.0-RC and new memory model: when it's enabled, global_queue is used for Dispatchers.Default and main queue for Dispatchers.Main