preface

Suspended functions are the hallmark of Kotlin coroutines. The suspend feature is also the most important feature, on which all other features are built. That’s why, in this article, we aim to get a deeper understanding of how it works.

Suspending a coroutine means to suspend it in its execution. It’s similar to how we stop playing PC games: you save and close the game, and then you and your computer go on to do something different. Then, after a while, you want to keep playing. So you reopen the game, restore the saved location, and continue playing from where you left off.

The scenario described above is a graphic metaphor for coroutines. They (tasks/pieces of code) can be interrupted (suspended to perform other tasks), and when they come back (task completion), they return with a Continuation (specifying where we recovered). We can use it (Continuation) to pick up where we left off.

Resume (Resume)

So let’s look at it in action. First, we need a coroutine block. The easiest way to create a coroutine is to write a suspend function directly. This code is our starting point:

suspend fun testCoroutine(a) {
    println("Before")

    println("After")}// Output in sequence
//Before
//After

Copy the code

The code above is simple: it prints “Before” and “After”. What happens if we suspend between two lines of code at this point? To achieve this effect, we can use the suspendCoroutine method provided by the Kotlin library:

suspend fun testCoroutine(a) {
    println("Before")

    suspendCoroutine<Unit> {
        
    }

    println("After")}// Output in sequence
//Before
Copy the code

If you call the code above, you will not see “After” and the code will run forever (that is, our testCoroutine method will not end). The coroutine is suspended after printing “Before”. Our code is about to break and will not be restored. So? How do we do that? Is there any mention of continuations?

Look again at the call to suspendCoroutine, and notice that it ends with a lambda expression. This method passes us an argument of type Continuation before suspending:

uspend fun testCoroutine(a) {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        println("Before too")
    }

    println("After")}// Output in sequence
//Before
//Before too
Copy the code

The above code adds that another method is called inside a lambda expression, well, that’s nothing new. This is similar to let, apply, etc. The suspendCoroutine method needs to be designed this way to get the continuation before the coroutine hangs. If the suspendCoroutine executes, it is too late, so the lambda expression will be invoked before the suspension. The advantage of this design is that you can restore or store continuations at some point. So we can let continuations resume immediately:

suspend fun testCoroutine(a) {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        continuation.resume(Unit)
    }

    println("After")}// Output in sequence
//Before
//After
Copy the code

We can also use it to start a new thread, and it takes a while to restore it:

suspend fun testCoroutine(a) {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        thread {
            Thread.sleep(1000)
            continuation.resume(Unit)
        }
    }

    println("After")}// Output in sequence
//Before
//(1 second later)
//After
Copy the code

This is an important finding. Note that the code to start a new thread can be referred to a method, and recovery can be triggered by a callback. In this case, the continuation is captured by a lambda expression:

fun invokeAfterSecond(operation: () -> Unit) {
    thread { 
        Thread.sleep(1000)
        operation.invoke()
    }
}

suspend fun testCoroutine(a) {
    println("Before")

    suspendCoroutine<Unit> { continuation ->
        invokeAfterSecond {
            continuation.resume(Unit)
        }
    }

    println("After")}// Output in sequence
//Before
//(1 second later)
//After
Copy the code

This mechanism works, but we don’t need to create threads to do the above. Threads are expensive, so why waste them? A better way is to set an alarm clock. On the JVM, we can use ScheduledExecutorService. We can use it to trigger a continuation. Resume (Unit)* after a certain amount of time:

private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply { isDaemon = true}}suspend fun testCoroutine(a) {
    println("Before")


    suspendCoroutine<Unit> { continuation ->
        executor.schedule({
            continuation.resume(Unit)},1000, TimeUnit.MILLISECONDS)
    }

    println("After")}// Output in sequence
//Before
//(1 second later)
//After
Copy the code

“Resume after a certain amount of time” seems like a very common feature. So let’s put it in a method and we’ll call it delay:

private val executor = Executors.newSingleThreadScheduledExecutor {
    Thread(it, "scheduler").apply { isDaemon = true}}suspend fun delay(time: Long) = suspendCoroutine<Unit> { cont ->
    executor.schedule({
       cont.resume(Unit)
    }, time, TimeUnit.MILLISECONDS)
}

suspend fun testCoroutine(a) {
    println("Before")

    delay(1000)

    println("After")}// Output in sequence
//Before
//(1 second later)
//After
Copy the code

In fact, the above code is an implementation of the Kotlin coroutine library Delay. Our implementation is more complex, mainly to support testing, but the essence of the idea is the same.

Output with value

One thing that may have puzzled you: Why do we pass Unit when we call the resume method? You might also ask why I write suspendCoroutine with the Unit type in front of it. It’s no coincidence that these two are actually the same type: one as an input type for resuming continuations, and one as a return value type for the suspendCoroutine method (specifying what type of value we want to return). The two types need to be the same:

val ret: Unit =
    suspendCoroutine<Unit> { continuation ->
        continuation.resume(Unit)}Copy the code

When we call suspendCoroutine, we determine the data type to be returned by the continuation recovery, and of course the data returned by the recovery is also returned by the suspendCoroutine method:

suspend fun testCoroutine(a) {

    val i: Int = suspendCoroutine<Int> { continuation ->
        continuation.resume(42)
    }
    println(i)/ / 42

    val str: String = suspendCoroutine<String> { continuation ->
        continuation.resume("Some text")
    }
    println(str)//Some text

    val b: Boolean = suspendCoroutine<Boolean> { continuation ->
        continuation.resume(true)
    }
    println(b)//true
}
Copy the code

The code above seems a little different from the games we talked about earlier, and none of the games allow you to carry something while resuming (unless you cheat or Google what the next challenge is). But the way the code above is designed to have a return value makes sense for coroutines. We often hang because we need to wait for some data. For example, we need to get data through an API network request, which is a very common scenario. A thread is processing business logic, and at some point, we need some data to proceed, at which point we request the data through the network library and return it to us. If there is no coroutine, the thread needs to stop and wait. This is a huge waste – thread resources are very expensive. This is especially true if the Thread is an important Thread, like the Main Thread in Android. With coroutines, however, the web request simply hangs, and we pass a continuation to the web request library with a self-introduction: “Once you’ve retrieved the data, drop it in my resume method.” Then the thread can do something else. Once the data is returned, the current or other method (depending on the Dispatcher we set) will resume execution from where the coroutine was suspended.

Following our wave of practice, we simulate our network library with a callback function:

data class User(val name: String)

fun requestUser(callback: (User) - >Unit) {
    thread { 
        Thread.sleep(1000)
        callback.invoke(User("hyy"))}}suspend fun testCoroutine(a) {
    println("Before")

    val user: User =
        suspendCoroutine<User> { continuation ->
            requestUser {
                continuation.resume(it)
            }
        }

    println(user)
    println("After")}// Output in sequence
//Before
//(1 second later)
//User(name=hyy)
//After
Copy the code

It is not convenient to call suspendCoroutine directly, we can extract a suspended function instead:

suspend fun requestUser(a): User {
    return suspendCoroutine<User> { continuation ->
        requestUser {
            continuation.resume(it)
        }
    }
}
suspend fun testCoroutine(a) {
    println("Before")

    val user = requestUser()

    println(user)
    println("After")}Copy the code

Nowadays, you rarely need to wrap callback functions to make them suspend functions, because many popular libraries (Retrofit, Room, etc.) already support suspend functions. On the other hand, we already know a little about the underlying implementation of those functions. It’s similar to what we just wrote. Is not the same, the underlying use suspendCancellableCoroutine function (cancelled). We’ll talk about that later.

suspend fun requestUser(a): User {
    return suspendCancellableCoroutine<User> { continuation ->
        requestUser {
            continuation.resume(it)
        }
    }
}
Copy the code

You might be wondering if the API does not return data but throws an exception, such as the service crashing or returning some error. In this case, instead of returning data, we need to throw an exception where the coroutine hangs. This is where we recover in abnormal situations.

Resume with exception

Each function we call may return some value or throw an exception. Like suspendCoroutine: returns normal when resume is called, and throws an exception at the suspension point when resumeWithException is called:

class MyException : Throwable("Just an exception")

suspend fun testCoroutine(a) {

    try {
        suspendCoroutine<Unit> { continuation ->
            continuation.resumeWithException(MyException())
        }
    } catch (e: MyException) {
        println("Caught!")}}//Caught
Copy the code

This mechanism is designed to deal with a variety of problems. For example, to mark a network exception:

suspend fun requestUser(a): User {
    return suspendCancellableCoroutine<User> { cont ->
        requestUser { resp ->
            if (resp.isSuccessful) {
                cont.resume(resp.data)}else {
                val e = ApiException(
                    resp.code,
                    resp.message
                )
                cont.resumeWithException(e)
            }
        }
    }
}
Copy the code

Translation is stuck… 😂, I think I’ll stop there.

At the end

I hope you now have a clear understanding of how suspend (pause) works from the user’s perspective. Best wishes!

Academy /article/cc-…