Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default dispatcher and UI dispatcher support for iOS #470

Open
pkliang opened this issue Jul 31, 2018 · 12 comments

Comments

@pkliang
Copy link

commented Jul 31, 2018

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

This comment has been minimized.

Copy link
Member

commented Jul 31, 2018

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

This comment has been minimized.

Copy link

commented Aug 20, 2018

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

This comment has been minimized.

Copy link

commented Aug 21, 2018

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

This comment has been minimized.

Copy link

commented Nov 19, 2018

@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

This comment has been minimized.

Copy link

commented Nov 19, 2018

@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

This comment has been minimized.

Copy link

commented Mar 12, 2019

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

This comment has been minimized.

Copy link
Member

commented Mar 13, 2019

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

This comment has been minimized.

Copy link

commented Jul 2, 2019

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?

@FrancoSabadini

This comment has been minimized.

Copy link

commented Jul 13, 2019

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

@chris-hatton

This comment has been minimized.

Copy link

commented Jul 15, 2019

@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.

@FrancoSabadini

This comment has been minimized.

Copy link

commented Jul 15, 2019

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

This comment has been minimized.

Copy link

commented Jul 16, 2019

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
9 participants
You can’t perform that action at this time.