Correct way to run coroutines in a specific threadpool/context

I have code here that uses JavaFX. In one case, one coroutine invokes a callback, and within that callback, I need to perform some operations on JavaFX widgets. However, that coroutine uses the Default dispatcher, and therefore uses a thread pool that is separate from the JavaFX main thread. And, JavaFX widgets may only be touched within the JavaFX main thread.

My solution so far has been to use withContext like this:

withContext(mainScope.coroutineContext) {
    // do JavaFX operations here
}

Where mainScope is a special JavaFX coroutine scope that is tied to JavafxApplication (see kotlinx-coroutines-javafx for more).

I doubt though that accessing coroutineContext like this is the way to go. This seems better:

mainScope.async {
    // do JavaFX operations here
}.await()

Is this the preferred method overall? Or am I missing some better way to achieve this?

I thought Dispatchers.Main is available in JavaFX applications, isn’t it?

Anyway, answering your question directly: using coroutineContext like this is probably not a good idea, because it may contain much more elements than just a dispatcher. My guess is that especially passing a Job to withContext() may cause weird behavior.

If I would have to use a dispatcher of some scope then I would first look into its documentation/source code to see what dispatcher it uses - maybe it is exposed publicly. If not, then you can acquire its dispatcher with:

val mainDispatcher = mainScope.coroutineContext[ContinuationInterceptor]

Oh, extracting the dispatcher like that, didn’t know it is possible to do that with public APIs! Interesting.

But what about my approach with async? Is this OK, or are there pitfalls as well?

If you use mainScope.async() (and probably withContext(mainScope.coroutineContext) as well) then the inner coroutine is not a child of the outer coroutine. It is a child of the provided scope/context (specifically: Job stored in this scope/context). As a result, structured concurrency is broken.

In some cases it will still work as expected, for example if the code inside async() throws an exception then it will be re-thrown from await(). In other cases, however, it won’t work as expected, for example cancellations won’t be forwarded from outer to inner coroutine:

val task = launch {
    mainScope.async {
        while (true) {
            println("I'm still working!")
            delay(500)
        }
    }.await()
}

delay(2000)

println("Cancelling")
task.cancel()

delay(2000)

Output:

I'm still working!
I'm still working!
I'm still working!
I'm still working!
Cancelling
I'm still working!
I'm still working!
I'm still working!
I'm still working!

Ahh, yes, that is a big problem. The ContinuationInterceptor based approach it is then.

BTW, out of curiosity, I checked the lifecycleScope in Android, since this one may cause similar issues (you can’t touch UI elements outside of the UI thread). I have to write this to get it to work:

withContext(lifecycleScope.coroutineContext[ContinuationInterceptor]!!) {
}

This is a bit verbose - perhaps a more succinct syntax could be possible in Kotlin coroutine for this in the future? Perhaps something like a dispatcher() extension function that extracts the dispatcher in this way? (Yeah, in this case, I could just use Dispatchers.Main, but I am talking in general here.)

I think accessing ContinuationInterceptor of a scope/cotext for such purposes should be considered a hack or workaround and we should try to avoid it if possible. This is probably why there is no such extension already.

In general, the proper way of accessing the main/UI thread is Dispatchers.Main or something similar.