Coroutine delay never finishes

I am faced with a strange situation within a coroutine:

I basically have this simple coroutine:

                    runBlocking {
                        launch {
                            repeat(3) {
                                println("running first... $it")
                                delay(500)
                            }
                        }
                    }
                    println("runBlocking ended")

Which runs fine, just printing:
running first… 0
running first… 1
running first… 2
runBlocking ended

Now I just add the dispatcher:

                    runBlocking {
                        launch(Dispatchers.Default) {
                            repeat(3) {
                                println("running first... $it")
                                delay(500)
                            }
                        }
                    }
                    println("runBlocking ended")

This will give me:
running first… 0

The program is blocked and delay never finishes.

If I try to reproduce this in a new project, I do not have this issue.

What could cause delay to never finish?

I am aware the information is scarce, but I have a complex JavaFX app and I call this onButtonAction at a time where the app is not doing anything.

Hmm… that’s strange. Do you use runBlocking() often in your code? Possibly from inside a coroutine? I’m thinking about the case where you blocked all threads of Dispatchers.Default.

1 Like

I am not using runBlocking at all, only for this test case.

I believe it is a bug.

https://youtrack.jetbrains.com/issue/IDEA-290112

I think not, because if we wrap this in the
CoroutineScope(Dispatchers.Default).launch
it works correctly

I think you really need to try to create a minimum reproducible example. Try to remove parts of your application one after another. Or the opposite: move code from your application to a new repo and see when the problem starts occurring.

There is always a possibility of a bug in coroutines, but most probably there is some code in your application that makes coroutines unresponsive. For example, if you block coroutine threads, then what you observe is a possible outcome.

Your bug report on YouTrack is confusing at least. If others can’t reproduce the problem, they can’t do too much about it.

edit:
I just noticed you said on YouTrack that you attached a sample project. But I guess you forgot to do this, because there are no files attached.

1 Like

Actually the file is attached. Maybe it is not visible publicly.
Happy to share.
CoroutineDelay_DemoCrash.zip (11.3 KB)

So, sorry for being a non-believer. I can reproduce it on my machine, the code is actually pretty simple and it really behaves strangely.

By looking into coroutines internals it seems delay() uses Dispatchers.Main if it can’t schedule a postponed task using the current dispatcher. It can be found here. And because you block the main thread with runBlocking() you basically get a deadlock.

I don’t know if this should be considered a bug in the coroutines lib or not. We should never block the thread running a coroutine and we should never block the main thread of the UI application. Otherwise, we can get into many weird deadlock situations - not only this one. So the bug is actually present in your own code.

Still, I agree this is quite confusing and surprising. I would never expect this code to behave like this.

2 Likes

While all this sounds plausible, it goes away when the dependency
implementation(“org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.6.0”)
is removed.

You may delete the CoroutineScope of FxToolBarController if it is in this example, it is not used anyway.

Yes, this is because this dependency provides/changes the value of Dispatchers.Main. delay() works like this:

  1. First, try the current dispatcher.
  2. If not possible, use Dispatchers.Main.
  3. If not possible, use DefaultExecutor.

Dispatchers.Default doesn’t support delays, so in your example it uses Dispatchers.Main which is JavaFX main thread, which is blocked by runBlocking().

If we remove the dependency, Dispatchers.Main disappears, so DefaultExecutor is used. If we use launch {} without a dispatcher, it uses the dispatcher created by runBloking() which supports delays, so it doesn’t get to Dispatchers.Main. If we replace launch() with CoroutineScope().launch {}, we no longer block runBlocking(), so the main thread is released.

Yes, confusing as hell :smiley:

(At least this is what I think is happening. I didn’t analyze it very thoroughly)

4 Likes

Interesting. Sounds very plausible, but I am still wondering. Because when I print the threads, I get:
Starting coroutine test
running first… 0 on thread JavaFX Application Thread
running second… 0 on thread DefaultDispatcher-worker-1
running first… 1 on thread JavaFX Application Thread
running first… 2 on thread JavaFX Application Thread

So while runBlocking is on the Main/JavaFX thread, that works fine. The delay does not work on the worker thread.
Changing that to Dispatchers.JavaFX, I get
Starting coroutine test
running first… 0 on thread JavaFX Application Thread
running first… 1 on thread JavaFX Application Thread
running first… 2 on thread JavaFX Application Thread

So in this case the second thread hangs right after the start.

Let’s see what JetBrains concludes.

I rewrote the function as a suspend function now and can supply any context without any issue with delay. Even Main and JavaFx.

Funny side note: I basically used runBlocking only because I was just testing something requiring a coroutine and all documentation examples do that to quickly provide a context.

Yes, but this is according to what I said, isn’t it? As long as runBlocking() is running, the main dispatcher can’t schedule anything. If we try to use launch(Dispatchers.Main), withContext(Dispatchers.Main) etc. it won’t execute. Dispatchers.Default works fine, but delay() internally falls back to Dispatchers.Main, so it is also blocked.

Your question is probably about using launch() without any dispatcher (your “first” example) - it works normally, even despite the fact it also uses the main thread. This is because the main thread isn’t really blocked. It is blocked from the main dispatcher perspective. runBlocking() “hijacks” the thread that invoked it and runs an event loop on it. As long as it has something to do, it occupies this thread entirely, so from the external perspective the thread is blocked. But internally the thread runs coroutines started inside runBlocking(). So in your “first” example coroutines are also scheduled on the JavaFX main thread, but not using the main dispatcher. They are scheduled using runBlocking internal dispatcher, which is not blocked and which in this specific case also uses the main thread.

1 Like

I just had the same issue in my real world application. In my personal opinion, Kotlin should not allow us to turn an arbitrary Executor which is not a ScheduledExecutorService into dispatcher. Running delay or withTimeout continuations on the main thread rather than the dispatcher itself is worse than a crash or compile time error.