4

I've looked at plenty of articles online but I'm still a bit confused as to what happens specifically, step by step, when suspend functions are suspended in coroutines.

I know that a suspend function, under the hood, is just a regular function with a Continuation parameter that allows it to resume, but my confusion is regarding where that suspend function or coroutine goes and where it comes back once resumed.

I've heard a few people saying "they don't necessarily come back to the same thread" and I don't get it, can someone please explain this to me step by step?

Ryan M
  • 18,333
  • 31
  • 67
  • 74
Cosovic
  • 79
  • 6
  • Does this answer your question? [What does suspend function mean in Kotlin Coroutine](https://stackoverflow.com/questions/47871868/what-does-suspend-function-mean-in-kotlin-coroutine) – Abuzaid Apr 23 '20 at 04:42
  • 1
    I don't think those questions really address the specific question about threading. – Ryan M Apr 23 '20 at 05:06

3 Answers3

3

TLDR;

There is no guarantee, it may or may not, it really depends that on the following points:

  1. Is the dispatcher is multi-threaded?
  2. Is there any dispatcher override in between?

LONG Answer

A coroutine has a CoroutineContext that specify how it behaves, where it run.

A CoroutineContext is mainly build up with four elements: Job, CoroutineName, CoroutineExceptionHandler and Dispatcher.

Its responsibility of the dispatcher to dispatch the coroutine. A dispatcher can be paused to stop coroutines to even run (this is useful in unit testing) mentioned here in the android conference talk, it may be a single-threaded dispatcher just like Dispatchers.Main, it has an event-loop like javascript has.

So, it really depends that on the following points:

  1. Is the dispatcher is multi-threaded?

For example: This will run on single thread.

suspend fun main() {
    val dispatcherScope = CoroutineScope(Executors.newSingleThreadExecutor().asCoroutineDispatcher())

    val job = dispatcherScope.launch {
        repeat(10) {
            launch {
                println("I'm working in thread ${Thread.currentThread().name}")
                // every coroutine on same thread
            }
        }
    }
    job.join()
}

Run it here, other single threaded dispatchers: Dispatchers.Main

  1. Is there any dispatcher override in between?

With the same context, if we override the dispatcher before launch, it will change the thread even if original context is based on single-threaded event-loop, each coroutine will run on different thread creating 10 different thread:

dispatcherScope.launch {
    repeat(10) {
        launch(Dispatchers.IO) {
            println("I'm working in thread ${Thread.currentThread().name}")
            // every coroutine on same thread
        }
    }
}

Run it here, other multi-threaded Dispatchers: Dispatchers.Default, Executor based dispatcher, Dispatchers.Unconfined (this launch coroutine in any free thread).

Animesh Sahu
  • 7,445
  • 2
  • 21
  • 49
1

The short answer is: they execute somewhere and come back with the result to somewhere.

The long(er) explanation for the short answer:

"Somewhere" might be the same thread, it might be a different thread - it depends on the dispatcher and in many cases, the current state of the dispatcher. For instance, the contents of a SequenceScope will (by default) run on the same thread.

Another case where a suspend function might run on the same thread is if it's using the same dispatcher as the calling function.

Dispatchers can also share threads between them to save on thread creation (and just keep a count of the maximum number of parallel operations for their own tasks), so even switching between different dispatchers that each use thread pools may not result in using a different thread.

As far as people saying "they don't necessarily come back to the same thread," this is correct, for similar reasons. Keep in mind that your dispatcher may have many threads, and the one that you were using before you got suspended might be occupied with a different function right now, so the dispatcher will simply pick a different thread to run your code on.

Ryan M
  • 18,333
  • 31
  • 67
  • 74
  • Thanks. I know the entire coroutine that the suspend fun is in gets paused until that suspension point is resolved (has its result), I'm just confused on whether that particular suspension point is paused when its suspended or if it simply just leaves the thread, does it's work on another thread and comes back with result to the same thread. I know the dispatcher doesn't specify any particular thread in IO or default but just a pool of those type of threads. Are the end of the day, it can't be magic, the coroutine needs a thread to work on. – Cosovic Apr 23 '20 at 06:27
  • If by "whether that particular suspension point is paused when its suspended" you mean whether the thread blocks to wait for it to finish whatever it's doing on the other thread (assuming it _does_ go to another thread), the answer would be no. The idea of a suspend function is precisely that it doesn't block, and it frees up the thread to do other work (and besides, in some cases it's not even going to come back to that thread). – Ryan M Apr 23 '20 at 08:57
1

When the coroutine suspends, the underlying Java method returns a special COROUTINE_SUSPENDED value. If the calling function is also suspendable, it also returns the object, and so the execution returns to the innermost plain, non-suspendable function. This function typically runs an event loop, where event handlers are calls to continuation.resume(). So now it is ready to take the next handler from the queue and resume another coroutine.

When you call continuation.resume(), the continuation itself knows about the dispatcher in charge of the coroutine and delegates to it. If the current thread isn't owned by that dispatcher, it dispatches an event to another event loop, the one served by the dispatcher's thread pool. This way the dispatcher controls the thread where the corutine resumes.

Marko Topolnik
  • 195,646
  • 29
  • 319
  • 436