11 minutes read

A coroutine is a part of a program that can be suspended and resumed later. Kotlin doesn't pause a coroutine at an arbitrary point, though; it can only do that when a special suspending function is called. It keeps track of suspending functions and can run another one that was waiting for execution and then resume the original function when possible, even without switching threads at OS level. This mechanism potentially allows us to run many more coroutines than the limit of the available threads. Let's see what it exactly means to us as developers.

Suspend function

Let's see how it works. When you call two simple functions, they run sequentially: a thread can execute only one function at a time:

sequential execution of functions

But what happens if a function becomes suspended? In this case, you can pause the execution of one function and run another one. The program will not forget about your paused function and will resume it later. This is especially useful when you need to wait for some external resource.

suspending one function and starting another

Use case example

Imagine we need to run a lot of "heavy" operations like calculating the checksum for hundreds of files we received from the internet to make sure they aren't broken (that's what package managers or torrent clients do all the time).

fun loadFile(): ByteArray {
    return ... // long operation, actual code skipped for brevity
}

fun calculateChecksum(fileData: ByteArray): String {
    return ... // another long operation, actual code skipped for brevity
}

fun main() {
    for (i in 1..10_000) {
        val fileData = loadFile()
        val checksum = calculateChecksum(fileData)
        println(checksum)
    }
}

To complete all calculations faster, we can process each file on a separate thread. However, it won't improve the performance if we have more files than the CPU cores because each core can only process one thread at a time. Moreover, if we have too many threads, we will run out of memory soon because each thread requires extra RAM just to start.

fun main() {
    for (i in 1..10_000) { // depends on the machine, rarely more than that
        Thread {
            val fileData = loadFileBlocking()
            val checksum = calculateChecksum(fileData)
            println(checksum)
        }.start()
    }
}

To fix the issue, more complex thread pools and callbacks are required. With coroutines, in contrast, we can run hundreds of thousands of parallel operations and keep our code simple. Kotlin will take care of suspending (pausing) those functions that are not active at the moment.

import kotlinx.coroutines.*

suspend fun loadFile(): ByteArray { // now it's a suspending function
    return ... // long operation
}

suspend fun calculateChecksum(fileData: ByteArray): String { // another suspending
    return ... // long operation
}

// 'runBlocking' and 'async' build coroutines, we'll learn about it in the next topic
//  so far you need to know that everything inside one of these builders is a coroutine
fun main() = runBlocking {
    for (i in 1..10_000) { // now we can have even more operations running concurrently
        async {            // async schedules background execution of a given coroutine
            val fileData = loadFile()
            val checksum = calculateChecksum(fileData)
            println(checksum)
        }
    }
}

Suspension points

As we know, Kotlin can't suspend our code at arbitrary points; it can do that (but doesn't have to) when we call a suspending function – a function marked with the suspend keyword. Such a call is a "parking point" for a coroutine.

JetBrains IDEs will mark these calls with a special symbol on the left.

Suspension point marker in IDE

So our last example can start multiple operations concurrently because it can suspend at the loadFile call in the first launched coroutine and give room for the next one. Replace the suspending loadFile function with a regular one and you'll notice that the code will become sequential again even though we've built a coroutine with the async call. That's because there is no more suspension point in the async coroutine, and it will not let any other coroutine start until it finishes. Coroutines need to "cooperate" to allow concurrency, and they cooperate via suspension. Cooperation is especially important for operations that do not require a lot of CPU but require a lot of waiting time for disk or networking requests (like our file loading). However, we can also use it for CPU-heavy tasks (like encoding) to introduce natural suspension points where an operation can be paused or canceled.

Suspend keyword

So which function can be marked as suspending? Any! Methods, top-level functions, extension functions, and even lambdas can be suspending. Here's the definition of the async function we've used:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
)

It declares the block parameter as a suspending extension function, and in our example we pass a three-line lambda expression to it. Note that we don't have to explicitly declare lambda as suspend, nor do we need to declare its parameters or return types.

Suspending function invocation

Suspending functions are great! So should we make all our functions suspending and use them everywhere? Not quite. Kotlin only allows calling suspending functions either from other suspending functions or from special coroutine builders like runBlocking and async (we'll learn more about them soon). And there is a reason for that: coroutines are cheap, but they are not free, and if we create them left and right, our code will become slower and harder to debug. So it's still a developer's job to figure out where we can benefit from suspending functions. For example:

  • when we wait for some slow device like networks or disks;

  • when a long-running operation can be potentially canceled in the middle.

Conclusion

For a quick recap, here are the main points you need to remember about suspending functions:

  • Coroutines are much cheaper than threads, but to work concurrently, they need to cooperate.

  • Coroutines cooperate by pausing at suspension points and letting others work.

  • Each call to a suspending function is a potential suspension point.

  • A suspending function can only be called from another suspending function or a coroutine builder.

65 learners liked this piece of theory. 0 didn't like it. What about you?
Report a typo