
suspend modifier in front of many functions. Such functions are called suspending functions, and they are the hallmark of Kotlin coroutines. Here is a simple example of a backend application using coroutines. Notice that the only difference between using coroutines and blocking threads is the suspend modifier.class GithubApi { @GET("orgs/{organization}/repos?per_page=100") suspend fun getOrganizationRepos( @Path("organization") organization: String ): List<Repo> } class GithubConnectorService( private val githubApi: GithubApi ) { suspend fun getKotlinRepos() = githubApi.getOrganizationRepos("kotlin") .map { it.toDomain() } } @Controller class UserController( private val githubConnectorService: GithubConnectorService, ) { @GetMapping("/kotlin/repos") suspend fun findUser(): GithubReposResponseJson = githubConnectorService.getKotlinRepos().toJson() }
suspend modifier in controller functions and calls suspending functions on a coroutine. On the other hand, suspending functions allow suspension, so network libraries like Retrofit[^103_5] react to the suspend modifier and suspend coroutines (instead of blocking threads) when they need to wait for a network response.main function. Such a function is wrapped by the Kotlin compiler and started in a coroutine. However, if we call another suspending function from main, this function will be called on the same coroutine. You can say that suspend functions are synchronized, but the simplest explanation is that they are not coroutines themselves; they can just suspend coroutines.import kotlinx.coroutines.* // Suspending function can suspend a coroutine suspend fun a() { // Suspends the coroutine for 1 second delay(1000) println("A") } // Suspending main is started by Kotlin in a coroutine suspend fun main() { println("Before") a() println("After") } // Before // (1 second delay) // A // After
Notice that if we had similar code in JavaScript but using async functions instead of suspending functions, the result would be "Before", "After", a 1-second delay, and then "A". This is because async functions in JavaScript are coroutines, and they always start asynchronous processes. Suspending functions are not coroutines; they are functions that can suspend coroutines. These two concepts should not be confused.
main function? For this, we can use the suspendCancellableCoroutine.import kotlinx.coroutines.* //sampleStart suspend fun main() { println("Before") suspendCancellableCoroutine<Unit> { } println("After") } // Before //sampleEnd
suspendCancellableCoroutineis a function from the kotlinx.coroutines library. Instead, we could use thesuspendCoroutinefunction from Kotlin standard library, which would behave the same in all examples in this chapter. I decided to usesuspendCancellableCoroutinebecause it is generally good practice to choose it as it supports cancellation and is more testable.
main function never finished). The coroutine is suspended after "Before". Our game has been stopped and never resumed. So, how can we resume?suspendCancellableCoroutine invocation and notice that it ends with a lambda expression ({ }). The function passed as an argument will be invoked before the suspension. This function has an argument of type Continuation. This is the object that we can use to resume the coroutine. We could use it to resume immediately[^103_2]:import kotlinx.coroutines.* import kotlin.coroutines.resume //sampleStart suspend fun main() { println("Before") suspendCancellableCoroutine<Unit> { continuation -> println("Before too") continuation.resume(Unit) } println("After") } // Before // Before too // After //sampleEnd
suspendCancellableCoroutine calls the lambda expression immediately, just like the let, apply, or useLines functions. This is necessary so that the continuation can be used just before the suspension. Calling the lambda expression after the suspendCancellableCoroutine call would be too late. So, the lambda expression passed as a parameter to the suspendCancellableCoroutine function is invoked just before the suspension. This lambda is used to store this continuation somewhere or to plan whether to resume it.Since Kotlin 1.3, the definition ofContinuationhas been changed. Instead ofresumeandresumeWithException, there is oneresumeWithfunction that expectsResult. TheresumeandresumeWithExceptionfunctions we are using are extension functions of the standard library that useresumeWith.
inline fun <T> Continuation<T>.resume(value: T): Unit = resumeWith(Result.success(value)) inline fun <T> Continuation<T>.resumeWithException( exception: Throwable ): Unit = resumeWith(Result.failure(exception))
import kotlinx.coroutines.delay import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume suspend fun a() { val a = "ABC" suspendCancellableCoroutine<Unit> { continuation -> // What is stored in the continuation? continuation.resume(Unit) } println(a) } suspend fun main() { val list = listOf(1, 2, 3) val text = "Some text" println(text) delay(1000) a() println(list) }
main function. So, this state must contain references to not only the local variables of the a function but also to the local variables of the main function - at least those used after the suspension. This is the only way to make it possible to resume the coroutine and continue from the point where it was suspended. So, the continuation must contain references to the values of the list and a variables. It must also keep the place where the coroutine was suspended, which means label 1 for function a, and label 2 for function main (because delay takes label 1). These are the essentials of explaining what a continuation is and how it works. The details are presented in the next chapter.
import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlin.coroutines.resume //sampleStart suspend fun main() { println("Before") suspendCancellableCoroutine<Unit> { continuation -> thread { println("Suspended") Thread.sleep(1000) continuation.resume(Unit) println("Resumed") } } println("After") } // Before // Suspended // (1 second delay) // After // Resumed //sampleEnd
import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlin.coroutines.* //sampleStart fun continueAfterSecond(continuation: Continuation<Unit>) { thread { Thread.sleep(1000) continuation.resume(Unit) } } suspend fun main() { println("Before") suspendCancellableCoroutine<Unit> { continuation -> continueAfterSecond(continuation) } println("After") } // Before // (1 sec) // After //sampleEnd
ScheduledExecutorService for this by setting it to call continuation.resume(Unit) after a defined amount of time.import java.util.concurrent.* import kotlinx.coroutines.* import kotlin.coroutines.resume //sampleStart private val executor = Executors.newSingleThreadScheduledExecutor { Thread(it, "scheduler").apply { isDaemon = true } } suspend fun main() { println("Before") suspendCancellableCoroutine<Unit> { continuation -> executor.schedule({ continuation.resume(Unit) }, 1000, TimeUnit.MILLISECONDS) } println("After") } // Before // (1 second delay) // After //sampleEnd
delay.import java.util.concurrent.* import kotlinx.coroutines.* import kotlin.coroutines.resume //sampleStart private val executor = Executors.newSingleThreadScheduledExecutor { Thread(it, "scheduler").apply { isDaemon = true } } suspend fun delay(timeMillis: Long): Unit = suspendCancellableCoroutine { cont -> executor.schedule({ cont.resume(Unit) }, timeMillis, TimeUnit.MILLISECONDS) } suspend fun main() { println("Before") delay(1000) println("After") } // Before // (1 second delay) // After //sampleEnd
delay function. This is much better than blocking one thread every time we need to wait for some time.delay from the Kotlin Coroutines library is implemented in older versions of this library. The current implementation is more complicated, mainly so it supports testing, but the essential idea remains the same.Unit to the resume function. You might also be wondering why we used Unit as a type argument for the suspendCancellableCoroutine. The fact that these two are the same is no coincidence.val ret: Unit = suspendCancellableCoroutine<Unit> { c -> c.resume(Unit) }
suspendCancellableCoroutine, we can specify which type will be returned in its continuation. The same type needs to be used when we call resume.import kotlinx.coroutines.* import kotlin.coroutines.resume //sampleStart suspend fun main() { val i: Int = suspendCancellableCoroutine<Int> { c -> c.resume(42) } println(i) // 42 val str: String = suspendCancellableCoroutine<String> { c -> c.resume("Some text") } println(str) // Some text val b: Boolean = suspendCancellableCoroutine<Boolean> { c -> c.resume(true) } println(b) // true } //sampleEnd
resume function". Then the thread can go do other things. Once the data is there, the thread will be used to resume from the point where the coroutine was suspended.requestUser that is implemented externally.import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlin.coroutines.resume data class User(val name: String) fun requestUser(callback: (User) -> Unit) { thread { Thread.sleep(1000) callback.invoke(User("Test")) } } //sampleStart suspend fun main() { println("Before") val user = suspendCancellableCoroutine<User> { cont -> requestUser { user -> cont.resume(user) } } println(user) println("After") } // Before // (1 second delay) // User(name=Test) // After //sampleEnd
suspendCancellableCoroutine directly is not convenient. We would prefer to have a suspending function instead. We can extract it ourselves.import kotlin.concurrent.thread import kotlinx.coroutines.* import kotlin.coroutines.resume data class User(val name: String) fun requestUser(callback: (User) -> Unit) { thread { Thread.sleep(1000) callback.invoke(User("Test")) } } //sampleStart suspend fun requestUser(): User { return suspendCancellableCoroutine<User> { cont -> requestUser { user -> cont.resume(user) } } } suspend fun main() { println("Before") val user = requestUser() println(user) println("After") } //sampleEnd
suspendCancellableCoroutine. When resume is called, it returns data passed as an argument. When resumeWithException is called, the exception that is passed as an argument is conceptually thrown from the suspension point.import kotlinx.coroutines.* import kotlin.coroutines.resumeWithException //sampleStart class MyException : Throwable("Just an exception") suspend fun main() { try { suspendCancellableCoroutine<Unit> { cont -> cont.resumeWithException(MyException()) } } catch (e: MyException) { println("Caught!") } } // Caught! //sampleEnd
suspend fun requestUser(): 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) } } } } suspend fun requestNews(): News { return suspendCancellableCoroutine<News> { cont -> requestNews( onSuccess = { news -> cont.resume(news) }, onError = { e -> cont.resumeWithException(e) } ) } }
import kotlinx.coroutines.* import kotlin.coroutines.* //sampleStart // Do not do this var continuation: Continuation<Unit>? = null suspend fun suspendAndSetContinuation() { suspendCancellableCoroutine<Unit> { cont -> continuation = cont } } suspend fun main() { println("Before") suspendAndSetContinuation() continuation?.resume(Unit) println("After") } // Before //sampleEnd
resume will never be called. You will only see "Before", and your program will never end unless we resume on another thread or another coroutine. To show this, we can set another coroutine to resume after a second.import kotlinx.coroutines.* import kotlin.coroutines.* //sampleStart // Do not do this, potential memory leak var continuation: Continuation<Unit>? = null suspend fun suspendAndSetContinuation() { suspendCancellableCoroutine<Unit> { cont -> continuation = cont } } suspend fun main(): Unit = coroutineScope { println("Before") launch { delay(1000) continuation?.resume(Unit) } suspendAndSetContinuation() println("After") } // Before // (1 second delay) // After //sampleEnd
[^103_3]: During a workshop discussion it turned out there is such a game: in Don't Starve Together, when you resume, you can change players. I haven't played it myself, but this sounds like a nice metaphor for resuming with a value.
[^103_4]: Suspending
main function is a special case. Kotlin compiler starts it in a coroutine.[^103_5]: If you decide what network client library to choose, instead of Retrofit, I recommend using Ktor Client. It is a modern, multiplatform, and coroutine-based library.