Coroutines

Context

Coroutines have to be run inside a context.

Suspend function

Suspend functions make sense to start only from coroutine (compilation requirements). And some functions (e.g. delay()) are making sense only inside of suspend functions.

Suspend function can be called from

  • another suspend function

  • from coroutine (This is the way actually to start a "first" suspend function)

Do not use blocking code (e.g. Thread.sleep()) in coroutine

Testing of suspend function

@Test
fun theTest() = runBlocking { // test should become a coroutine to be able to call suspend function
    doWork()    // assuming that doWork is a suspend function
    //assert bla
}

Builders

Launch

Launch creates a coroutine and submit it to the separate thread, main thread will not wait until the coroutine is finished.

launch { 
    delay(1000) // non blocking the thread, it suspends the courutine
    println("")
}

runBlocking

This can be useful to use in tests (to make test a coroutine to be able to invoke suspend function in the test).

runBlocking is used as a bridge between regular and suspending functions, or between the blocking and non-blocking worlds. It works as an adaptor for starting the top-level main coroutine. It is intended primarily to be used in main() functions and tests.

runBlocking {        // main thread will wait until this coroutine is finished
    delay(1500)
}

async

async starts a new coroutine and returns a Deferred object. Deferred represents a concept known by other names such as Future or Promise. It stores a computation, but it defers the moment you get the final result; it promises the result sometime in the future.

The main difference between async and launch is that launch is used to start a computation that isn't expected to return a specific result. launch returns a Job that represents the coroutine. It is possible to wait until it completes by calling Job.join().

Deferred is a generic type that extends Job. An async call can return a Deferred<Int> or a Deferred<CustomType>, depending on what the lambda returns (the last expression inside the lambda is the result).

To get the result of a coroutine, you can call await() on the Deferred instance. While waiting for the result, the coroutine that this await() is called from is suspended:

import kotlinx.coroutines.*

fun main() = runBlocking {
    val deferred: Deferred<Int> = async {
        loadData()
    }
    println("waiting...")
    println(deferred.await())
}

suspend fun loadData(): Int {
    println("loading...")
    delay(1000L)
    println("loaded!")
    return 42
}

Structured concurrency

The coroutine scope is responsible for the structure and parent-child relationships between different coroutines. New coroutines usually need to be started inside a scope.

When launch, async, or runBlocking are used to start a new coroutine, they automatically create the corresponding scope. All of these functions take a lambda with a receiver as an argument, and CoroutineScope is the implicit receiver type:

launch { /* this: CoroutineScope */ }
import kotlinx.coroutines.*

fun main() = runBlocking { /* this: CoroutineScope */
    launch { /* ... */ }
    // the same as:
    this.launch { /* ... */ }
}

Control

Join

Similar to joining a thread. The calling thread blocks until coroutine is finished.

Launch returns Job, which has join method. We can also check is coroutine is finished.

val job = launch { 
    println("")
}
job.join() // join is a suspend function

Cancel

In order to be cancellable coroutine should be able to check for cancellation.

E.g. delay() function knows how to cancel.

fun main() = runBlocking {
     val job = launch {
         repeat(1000) {
            delay(1)          // delay knows how to cancel, so cancellation will work
            print(".")
         }
    }
    delay(15)
    job.cancelAndJoin()
    print("Done")
}
fun main() = runBlocking {
     val job = launch {
         repeat(1000) {
            Thread.sleep(1) // this function does not know how to cancel, so cancellation will not work
            print(".")
         }
    }
    delay(15)
    job.cancelAndJoin()
    print("Done")
}

It is possible to check the status itself (it did not work on my machine, but I took it from the course)

fun main() = runBlocking {
     val job = launch {
         repeat(1000) {
             if (!isActive) throw CancellationException()
             sleep(10)
             print(".")
         }
    }
    delay(200)
    job.cancelAndJoin()
    print("Done")
}

Last updated

Was this helpful?