If you notice the functions closely, they can be used to resume the coroutine with a return value or with an exception if an error had occurred while the function was suspended. This way, a function could be started, paused, and resume with the help of Continuation. We just have to use the suspend keyword.
The suspend keyword means that this function can be blocking. Such a function can suspend a buildSequence coroutine. Suspending functions can be created as standard Kotlin functions, but we need to be aware that we can only call them from within a coroutine. Otherwise, we'll get a compiler error.
With coroutines, it just suspends and gives the library a continuation with the instruction "Once you've got this data, just send it to the 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.
Suspending functions are at the center of everything coroutines. A suspending function is simply a function that can be paused and resumed at a later time. They can execute a long running operation and wait for it to complete without blocking.
The syntax of a suspending function is similar to that of a regular function except for the addition of the suspend
keyword. It can take a parameter and have a return type. However, suspending functions can only be invoked by another suspending function or within a coroutine.
suspend fun backgroundTask(param: Int): Int {
// long running operation
}
Under the hood, suspend functions are converted by the compiler to another function without the suspend keyword, that takes an addition parameter of type Continuation<T>
. The function above for example, will be converted by the compiler to this:
fun backgroundTask(param: Int, callback: Continuation<Int>): Int {
// long running operation
}
Continuation<T>
is an interface that contains two functions that are invoked to resume the coroutine with a return value or with an exception if an error had occurred while the function was suspended.
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
But what does suspend mean?
Functions marked with the suspend
keyword are transformed at compile time to be made asynchronous under the hood, even though they appear synchronous in the source code.
The best source to understand this transformation IMO is the talk "Deep Dive into Coroutines" by Roman Elizarov.
This includes the following changes to the function:
Unit
, which is how Kotlin represents void functionsContinuation<X>
argument (where X is the former return type of the function that was declared in the code). This continuation acts like a callback.Continuation
object.This is a very quick way to describe it, but you can see it happen with more details and with examples in the talk. This whole transformation is basically how the "suspend/resume" mechanism is implemented under the hood.
Coroutine or function gets suspended?
At a high level, we say that calling a suspending function suspends the coroutine, meaning the current thread can start executing another coroutine. So, the coroutine is said to be suspended rather than the function.
In fact, call sites of suspending functions are called "suspension points" for this reason.
Which coroutine gets suspended?
Let's look at your code and break down what happens:
// 1. this call starts a new coroutine (let's call it C1).
// If there were code after it, it would be executed concurrently with
// the body of this async
async {
...
// 2. this is a regular function call, so we go to computation()'s body
val deferred = computation()
// 4. because await() is suspendING, it suspends coroutine C1.
// This means that if we had a single thread in our dispatcher,
// it would now be free to go execute C2
// 7. once C2 completes, C1 is resumed with the result `true` of C2's async
val result = deferred.await()
...
// 8. C1 can now keep going in the current thread until it gets
// suspended again (or not)
}
fun computation(): Deferred<Boolean> {
// 3. this async call starts a second coroutine (C2). Depending on the
// dispatcher you're using, you may have one or more threads.
// 3.a. If you have multiple threads, the block of this async could be
// executed in parallel of C1 in another thread
// 3.b. If you have only one thread, the block is sort of "queued" but
// not executed right away (as in an event loop)
//
// In both cases, we say that this block executes "concurrently"
// with C1, and computation() immediately returns the Deferred
// instance to its caller (unless a special dispatcher or
// coroutine start argument is used, but let's keep it simple).
return async {
// 5. this may now be executed
true
// 6. C2 is now completed, so the thread can go back to executing
// another coroutine (e.g. C1 here)
}
}
The outer async
starts a coroutine. When it calls computation()
, the inner async
starts a second coroutine. Then, the call to await()
suspends the execution of the outer async
coroutine, until the execution of the inner async
's coroutine is over.
You can even see that with a single thread: the thread will execute the outer async
's beginning, then call computation()
and reach the inner async
. At this point, the body of the inner async is skipped, and the thread continues executing the outer async
until it reaches await()
.
await()
is a "suspension point", because await
is a suspending function.
This means that the outer coroutine is suspended, and thus the thread starts executing the inner one. When it is done, it comes back to execute the end of the outer async
.
Does suspend mean that while outer async coroutine is waiting (await) for the inner computation coroutine to finish, it (the outer async coroutine) idles (hence the name suspend) and returns thread to the thread pool, and when the child computation coroutine finishes, it (the outer async coroutine) wakes up, takes another thread from the pool and continues?
Yes, precisely.
The way this is actually achieved is by turning every suspending function into a state machine, where each "state" corresponds to a suspension point inside this suspend function. Under the hood, the function can be called multiple times, with the information about which suspension point it should start executing from (you should really watch the video I linked for more info about that).
To understand what exactly it means to suspend a coroutine, I suggest you go through this code:
import kotlinx.coroutines.Dispatchers.Unconfined
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
var continuation: Continuation<Int>? = null
fun main() {
GlobalScope.launch(Unconfined) {
val a = a()
println("Result is $a")
}
10.downTo(0).forEach {
continuation!!.resume(it)
}
}
suspend fun a(): Int {
return b()
}
suspend fun b(): Int {
while (true) {
val i = suspendCoroutine<Int> { cont -> continuation = cont }
if (i == 0) {
return 0
}
}
}
The Unconfined
coroutine dispatcher eliminates the magic of coroutine dispatching and allows us to focus directly on bare coroutines.
The code inside the launch
block starts executing right away on the current thread, as a part of the launch
call. What happens is as follows:
val a = a()
b()
, reaching suspendCoroutine
.b()
executes the block passed to suspendCoroutine
and then returns a special COROUTINE_SUSPENDED
value. This value is not observable through the Kotlin programming model, but that's what the compiled Java method does.a()
, seeing this return value, itself also returns it.launch
block does the same and control now returns to the line after the launch
invocation: 10.downTo(0)...
Note that, at this point, you have the same effect as if the code inside the launch
block and your fun main
code are executing concurrently. It just happens that all this is happening on a single native thread so the launch
block is "suspended".
Now, inside the forEach
looping code, the program reads the continuation
that the b()
function wrote and resumes
it with the value of 10
. resume()
is implemented in such a way that it will be as if the suspendCoroutine
call returned with the value you passed in. So you suddenly find yourself in the middle of executing b()
. The value you passed to resume()
gets assigned to i
and checked against 0
. If it's not zero, the while (true)
loop goes on inside b()
, again reaching suspendCoroutine
, at which point your resume()
call returns, and now you go through another looping step in forEach()
. This goes on until finally you resume with 0
, then the println
statement runs and the program completes.
The above analysis should give you the important intuition that "suspending a coroutine" means returning the control back to the innermost launch
invocation (or, more generally, coroutine builder). If a coroutine suspends again after resuming, the resume()
call ends and control returns to the caller of resume()
.
The presence of a coroutine dispatcher makes this reasoning less clear-cut because most of them immediately submit your code to another thread. In that case the above story happens in that other thread, and the coroutine dispatcher also manages the continuation
object so it can resume it when the return value is available.
As many good answers are already there, I would like to post a simpler example for others.
runBlocking use case :
suspend
functionrunBlocking { }
starts a Coroutine in blocking way. It is similar to how we were blocking normal threads with Thread
class and notifying blocked threads after certain events. runBlocking { }
does block the current executing thread, until the coroutine (body between {}
) gets completed
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
runBlocking {
Log.d(TAG,"Inner code started on Thread : " + Thread.currentThread().name + " making outer code suspend");
myMethod();
}
Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
}
private suspend fun myMethod() {
withContext(Dispatchers.Default) {
for(i in 1..5) {
Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
}
}
This outputs :
I/TAG: Outer code started on Thread : main
D/TAG: Inner code started on Thread : main making outer code suspend
// ---- main thread blocked here, it will wait until coroutine gets completed ----
D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- main thread resumes as coroutine is completed ----
I/TAG: Outer code resumed on Thread : main
launch use case :
launch { }
starts a coroutine concurrently. worker
thread.worker
thread and outer thread (from which we called launch { }
) both runs concurrently. Internally, JVM may perform Preemptive Threading
When we require multiple tasks to run in parallel, we can use this. There are scopes
which specify lifetime of coroutine. If we specify GlobalScope
, the coroutine will work until application lifetime ends.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
GlobalScope.launch(Dispatchers.Default) {
Log.d(TAG,"Inner code started on Thread : " + Thread.currentThread().name + " making outer code suspend");
myMethod();
}
Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
}
private suspend fun myMethod() {
withContext(Dispatchers.Default) {
for(i in 1..5) {
Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
}
}
}
This Outputs :
10806-10806/com.example.viewmodelapp I/TAG: Outer code started on Thread : main
10806-10806/com.example.viewmodelapp I/TAG: Outer code resumed on Thread : main
// ---- In this example, main had only 2 lines to execute. So, worker thread logs start only after main thread logs complete
// ---- In some cases, where main has more work to do, the worker thread logs get overlap with main thread logs
10806-10858/com.example.viewmodelapp D/TAG: Inner code started on Thread : DefaultDispatcher-worker-1 making outer code suspend
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-1
10806-10858/com.example.viewmodelapp D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-1
async and await use case :
async
and await
would help. 2
suspend functions myMethod() and myMethod2(). myMethod2()
should get executed only after full completion of myMethod()
OR myMethod2()
depends on result of myMethod()
, we can use async
and await
async
starts a coroutine in parallel similar to launch
. But, it provides a way to wait for one coroutine before starting another coroutine in parallel.That way is await()
. async
returns an instance of Deffered<T>
. T
would be Unit
for default. When we need to wait for any async
's completion, we need to call .await()
on Deffered<T>
instance of that async
. Like in below example, we called innerAsync.await()
which implies that the execution would get suspended until innerAsync
gets completed. We can observe the same in output. The innerAsync
gets completed first, which calls myMethod()
. And then next async
innerAsync2
starts, which calls myMethod2()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
Log.i(TAG,"Outer code started on Thread : " + Thread.currentThread().name);
job = GlobalScope.launch(Dispatchers.Default) {
innerAsync = async {
Log.d(TAG, "Inner code started on Thread : " + Thread.currentThread().name + " making outer code suspend");
myMethod();
}
innerAsync.await()
innerAsync2 = async {
Log.w(TAG, "Inner code started on Thread : " + Thread.currentThread().name + " making outer code suspend");
myMethod2();
}
}
Log.i(TAG,"Outer code resumed on Thread : " + Thread.currentThread().name);
}
private suspend fun myMethod() {
withContext(Dispatchers.Default) {
for(i in 1..5) {
Log.d(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
}
}
}
private suspend fun myMethod2() {
withContext(Dispatchers.Default) {
for(i in 1..10) {
Log.w(TAG,"Inner code i : $i on Thread : " + Thread.currentThread().name);
}
}
}
This outputs :
11814-11814/? I/TAG: Outer code started on Thread : main
11814-11814/? I/TAG: Outer code resumed on Thread : main
11814-11845/? D/TAG: Inner code started on Thread : DefaultDispatcher-worker-2 making outer code suspend
11814-11845/? D/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-2
11814-11845/? D/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-2
// ---- Due to await() call, innerAsync2 will start only after innerAsync gets completed
11814-11848/? W/TAG: Inner code started on Thread : DefaultDispatcher-worker-4 making outer code suspend
11814-11848/? W/TAG: Inner code i : 1 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 2 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 3 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 4 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 5 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 6 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 7 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 8 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 9 on Thread : DefaultDispatcher-worker-4
11814-11848/? W/TAG: Inner code i : 10 on Thread : DefaultDispatcher-worker-4
I've found that the best way to understand suspend
is to make an analogy between this
keyword and coroutineContext
property.
Kotlin functions can be declared as local or global. Local functions magically have access to this
keyword while global don't.
Kotlin functions can be declared as suspend
or blocking. suspend
functions magically have access to coroutineContext
property while blocking functions don't.
The thing is: coroutineContext
property
is declared like a "normal" property in Kotlin stdlib but this declaration is just a stub for documentation/navigation purposes. In fact coroutineContext
is builtin intrinsic property that means under the hood compiler magic aware of this property like it aware of language keywords.
What this
keyword does for local functions is what coroutineContext
property does for suspend
functions: it gives access to current context of execution.
So, you need suspend
to get an access to coroutineContext
property - the instance of currently executed coroutine context
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With