Coroutines That Call Functions With Blocking Code

I’m trying to confirm if my understanding of coroutines is correct.

Given that: methodThatContainsBlockingCode(i) calls some sort of blocking code, be it jdbc, REST call, etc…
Then 10 lightweight threads will be created and block execution till the last one completes

// methodThatContainsBlockingCode(i) is a 'mocked' function that calls Thread.sleep(3_000)
suspend fun process() : Unit {
    withContext(Dispatchers.IO) {
        for (i in 1 .. 10) {            
            async() { methodThatContainsBlockingCode(i) }            

When I see the code execute it takes like 3 to 4 seconds total for them to complete foo() to return the calling function. This is great, as without the coroutine it would take > 30 seconds.

Some reading/videos/medium posts have lead me to believe that I’m going to have to wait for the methods in the coroutines to finish before the method returns? Is this correct?

Is there a way to immediately return where the code will continue to execute? The reason I’m asking is because foo() is being called from a Spring RestController and I would really like to immediately return to the client so the client does not have to wait. I’m dealing with legacy code here and refactoring to a reactive approach is not feasible.

FYI, I did get this to work w/ a CompletableFuture but it seems the completeOnTimeOut method doesn’t execute :frowning:

    fun foo(request: TheRequest) {

        val ids = checkIds(request.ids)

        ids.forEach {
  "Executing work with id: $it")
            }.thenAccept{ barService.logWorkComplete(it) }
                //.completeOnTimeout(timeOutError(), 1, TimeUnit.HOURS)
1 Like

You don’t really use any features of coroutines here. As a matter of fact, you opted-out of their most important benefits. If we need to simply schedule background I/O operations and we don’t care when and if they complete, then the easiest way with coroutines is GlobalScope.launch(Dispatchers.IO) {}. But it is not much different than creating our own executor with: Executors.newFixedThreadPool() and submitting to it, or even doing simply: thread {}.

CompletableFuture.supplyAsync{} is actually not a good idea here, because it uses ForkJoinPool.commonPool() and we should not block threads in this thread pool.

1 Like

Thank you @broot . Please forgive my ignorance as I am new to kotlin and even newer to coroutines. What are the important features that I’m opting out of?

The code example in the first snippet is taken from a couple different tutorials.

I have seen the usage of GlobalScope.launch(Dispatchers.IO) {} but I’m concerned about using that in a HTTP request. Especially if a request to the endpoint is made when another request is running. Am I overthinking that?

We benefit from coroutines the most if we have multiple tasks that often have to wait for each other, so they should be synchronized. Or if we frequently switch between I/O, CPU-intensive processing and/or event loops. In your case you simply delegate an I/O operation to a background thread - coroutines framework internally will do pretty much the same we always did when using executors and thread pools.

Coroutines also provide structured concurrency which means we can easily wait for other tasks, we automatically propagate errors and cancellations, etc., but you said you actually like to avoid that.

What is your concern exactly?

1 Like

The use of a GlobalScope scope seems dangerous in an HTTP context. But then again, I’m new to Kotlin and coroutines so maybe I’m over thinking it?

Anyways, is my understanding correct that the blocking code called in a coroutine will block the coroutine from finishing and I will have to wait for the code to finish before any follow on code is executed?

Thank you for your time!

You didn’t really explain what do you mean. You only repeated what you already said - that it is somehow “dangerous”. How is it dangerous?

I think I don’t get your question. Yes, this is how programming languages work in general - we execute a line of code and when we finish, we execute the next one. Coroutines aren’t any different.

If you meant the code that invoked GlobalScope.launch(), the no, it doesn’t wait for the launched coroutine to finish.

I’ll chime in and see if I can give a real quick Coroutines 101 crash course.

The structured concurrency stuff that broot is talking about is (as far as I understand) a fancy, complicated way of saying that “by design, when you start a coroutine, the method that started it won’t finish until the coroutine does”. A quick example:

suspend fun myCoroutineMethod() {
    coroutineScope {
        launch { doAThing() } // Launch returns a Job we can wait for or check status or whatever

In the above code, we don’t explicitly wait for the launch { doAThing() } to finish, but the way coroutines work, the coroutineScope lambda won’t finish until the launch has finished.

Using GlobalScope is discouraged, because it is going against that fundamental coroutine philosophy of “you wait for what you start”. If you’re using GlobalScope, you are effectively firing and forgetting, which means if the coroutine you launch fails, or runs forever… what happens? Do you deal with it? So ideally you should know what you’re doing before using GlobalScope.

I think another thing that broot is saying is that one of the strengths of coroutines is that, as long as you’re not using blocking code, you don’t have to worry about thread management or threads at all, really. You can just start firing off coroutines in your method, write other code, and it all just works. But in this case, the only thing you’re trying to do is launch a task in the background, so you don’t really need to use coroutines or get any benefit out of it. Using something like a CompletableFuture, or even submitting the work to an ExecutorService might be a better choice.

If you give us more context over what you’re trying to do, we can probably provide better guidance. :slight_smile:

Nitpick: we do wait for launch (docs) to finish. :slight_smile: But it finishes rather quickly, as it launches doAThing() in the background and then returns without caring for the result of doAThing() - like when using Promise / Future (although async (docs) would be the better analogy here).

We do not wait for doAThing() to finish, before executing doAnotherThing() and then doAThirdThing(), but as already said, coroutineScope (docs) will wait for every child to finish, so it and in turn myCoroutineMethod will return only when doAThirdThing() and doAThing() have returned (or thrown), in whatever order.

About GlobalScope (docs): it’s marked as @DelicateCoroutinesApi, so not really deprecated, but also maybe not really suitable for common use. The caveat mentioned in the docs is that GlobalScope is (as the name implies) valid and running throughout the lifetime of your JVM application. So it could be that an operation, e.g. network IO, is blocked forever / very long, which won’t get cleaned up by structured concurrency (what is normally used when programming with Kotlin coroutines).

If you have a matching lifecycle where you can start and end a coroutine scope (which is the thing where a group of coroutines is managed in, so to say), the better aproach would be to create your own scope and end it appropriately. You can create one at the start of your lifecycle with CoroutineScope() (docs) and then at the end of your lifecycle cancel() (docs) it.

Regarding blocking[1] stuff: the thread on which a coroutine (your lines of code) is actually executed will be determined by a CoroutineDispatcher (docs), which is part of a coroutine context and thus can be changed via e.g. withContext(Dispatchers.IO) { ... }.

There is Dispatchers.Default (docs), which is used by default (surprise!). It spawns a number of threads on which coroutines can run (between 2 and #cores[2]). The main thing to note here is: if you schedule a blocking operation on such a thread, the thread will be blocked, so you have one less slot for a coroutine that could run in parallel. It is important to schedule only such coroutines onto this dispatcher, which may suspend and do not block, or else Dispatchers.Default will be starved of slots to run coroutines in and it will halt.

Luckily there is Dispatchers.IO (docs) which is intended for blocking operations, such as (non-async) IO - hence the name. It spawns up to 64 or #cores threads on demand (whichever is higher, so probably 64). This is an extra pool of slots where coroutines can run in. Note that this one can also fill up, but if 64 IO operations are pending, then something severe might be wrong… (you should always set reasonable timeouts on blocking operations).

That’s why it’s important to use e.g. withContext(Dispatchers.IO) { ... } to separate the blocking things (like calling a coroutine-unaware library which does something blocking under the hood, be it in Java or Kotlin) from the ‘normal’ coroutine things.

I hope this clears things up a bit and removes some concerns. For more information, I suggest reading through the majority of the official Coroutines Guide (up until Select expression).

  1. blocking here means that a whole thread cannot progress further ↩︎

  2. number of CPU cores ↩︎

I had to read your nitpick a few times to figure out what you were even nitpicking. I think what I wrote is clear, but now that I understand where you’re coming from, I’ll re-word what I said to make it clearer.

When we use launch { ... }, it returns a Job, similar to how if we use an ExecutorService and call submit, it returns a Future. The difference is that with a coroutine Job, even if we don’t call join(), the surrounding CoroutineScope will wait for the Job to finish, because the Job is effectively attached to that CoroutineScope. Where as with a Future, if you don’t call get(), your code that submitted the work to the ExecutorService will finish and return, and the Future will be left running with nobody caring whether it ever completes, or if it completes successfully or with an error. You can fire off work with coroutines to run in the background, but you opt-in to that, rather than with Futures where that’s the default behaviour, and you have to remember to opt-in to waiting for the work to complete.