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?
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.
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?
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()
}
}
}
@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
@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
}
}
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?
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.
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?
I'm having this same issue, as @sschilli mentioned, is there an example on how to use the MainLoopDispatcher?
@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.
Thanks @chris-hatton, I figured out how to do it a couple of days back but that project is very useful! Thanks for sharing!
@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.
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?
Most helpful comment
@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.