Search code examples
multithreadingkotlinkotlin-coroutinescoroutinesuspend

Kotlin - Suspend functions in one threaded environments


I'm not entirely sure if my mental model of suspend is correct. From what I gathered it seems to mean that a (long running) suspend function can be suspended if another function inside it is marked with suspend (which generates a suspension point for the parent function).

To keep it simple lets assume a one threaded environment without asynchronous programming.

launch { //<--- creates a coroutine in which we can use suspend functions
    fetchUserData("Jon") 
}


// and following functions:

suspend fun fetchUserData(userName: String) {
    makeLongRunningNetworkCall(userName) //<---- suspends fetchUserData()
}

suspend fun makeLongRunningNetworkCall(userName: String) {...}

My understanding is, that makeLongRunningNetworkCall() "takes" fetchUserData() "off" the thread so it doesn't block other computations while waiting for the results of makeLongRunningNetworkCall().

But if nothing suspends makeLongRunningNetworkCall() isn't the thread still blocked by makeLongRunningNetworkCall()? I mean the "waiting" for the network result has to be done somewhere or else the result might be missed?

So for me suspend in that case would just make sense if fetchUserData() and makeLongRunningNetworkCall() would run on different threads, so that makeLongRunningNetworkCall() tells its parent function to go home and free its thread until it received a result?!

Is my understanding correct? Or does suspend rather mean the whole coroutine is taken off the thread? But then again, who assures the response of the network call is captured?


Solution

  • Is my understanding correct? Or does suspend rather mean the whole coroutine is taken off the thread?

    Rather the second, but in order to see the difference, you'll need to add some code in fetchUserData. For instance, consider:

    suspend fun fetchUserData(userName: String): UserData {
        val userData = makeLongRunningNetworkCall(userName)
        return useData
    }
    

    If makeLongRunningNetworkCall suspends, then fetchUserData does need to wait for it to resume before executing the rest of its code (the return). Similarly, the caller of fetchUserData also needs to wait, etc. This is why it's easy to reason about suspend functions - they actually run sequentially in that sense.

    So, with that in mind, the whole coroutine is suspended (the whole stack up to the initial coroutine builder launch), because nothing in the execution stack will carry on until makeLongRunningNetworkCall resumes.

    But then again, who assures the response of the network call is captured?

    This is a good question. All of the above actually started with the assumption that makeLongRunningNetworkCall does suspend. This is not an abstract concept. What it means concretely is that the function will return (as in, actually return) a special token called COROUTINE_SUSPENDED, so the whole suspension mechanism happens, and the thread starts executing something else.

    This means that this function must be cooperative and suspend when it's possible. If the function actually blocks on a network call, then it doesn't actually suspend, and it really does block the thread as you guessed. When functions like this actually suspend, it often means that they offloaded their blocking work to another thread, or that they are truly non-blocking (e.g. callback-based) - but sometimes that just means the offloading happens deeper.

    To understand and demystify how this works, I suggest reading this nice article about how kotlinx.coroutines is built on just a couple compiler built-ins: https://blog.kotlin-academy.com/kotlin-coroutines-animated-part-1-coroutine-hello-world-51797d8b9cd4