Skip to content

Latest commit

 

History

History
416 lines (305 loc) · 13.5 KB

basics.md

File metadata and controls

416 lines (305 loc) · 13.5 KB

Table of contents

Coroutine Basics

This section covers basic coroutine concepts.

Your first coroutine

Run the following code:

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

You can get full code here.

You will see the following result:

Hello,
World!

Essentially, coroutines are light-weight threads. They are launched with launch coroutine builder in a context of some CoroutineScope. Here we are launching a new coroutine in the GlobalScope, meaning that the lifetime of the new coroutine is limited only by the lifetime of the whole application.

You can achieve the same result replacing GlobalScope.launch { ... } with thread { ... } and delay(...) with Thread.sleep(...). Try it.

If you start by replacing GlobalScope.launch by thread, the compiler produces the following error:

Error: Kotlin: Suspend functions are only allowed to be called from a coroutine or another suspend function

That is because delay is a special suspending function that does not block a thread, but suspends coroutine and it can be only used from a coroutine.

Bridging blocking and non-blocking worlds

The first example mixes non-blocking delay(...) and blocking Thread.sleep(...) in the same code. It is easy to lose track of which one is blocking and which one is not. Let's be explicit about blocking using runBlocking coroutine builder:

import kotlinx.coroutines.*

fun main() { 
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    } 
}

You can get full code here.

The result is the same, but this code uses only non-blocking delay. The main thread invoking runBlocking blocks until the coroutine inside runBlocking completes.

This example can be also rewritten in a more idiomatic way, using runBlocking to wrap the execution of the main function:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // start main coroutine
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}

You can get full code here.

Here runBlocking<Unit> { ... } works as an adaptor that is used to start the top-level main coroutine. We explicitly specify its Unit return type, because a well-formed main function in Kotlin has to return Unit.

This is also a way to write unit tests for suspending functions:

class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> {
        // here we can use suspending functions using any assertion style that we like
    }
}

Waiting for a job

Delaying for a time while another coroutine is working is not a good approach. Let's explicitly wait (in a non-blocking way) until the background Job that we have launched is complete:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes
//sampleEnd    
}

You can get full code here.

Now the result is still the same, but the code of the main coroutine is not tied to the duration of the background job in any way. Much better.

Structured concurrency

There is still something to be desired for practical usage of coroutines. When we use GlobalScope.launch, we create a top-level coroutine. Even though it is light-weight, it still consumes some memory resources while it runs. If we forget to keep a reference to the newly launched coroutine it still runs. What if the code in the coroutine hangs (for example, we erroneously delay for too long), what if we launched too many coroutines and ran out of memory? Having to manually keep references to all the launched coroutines and join them is error-prone.

There is a better solution. We can use structured concurrency in our code. Instead of launching coroutines in the GlobalScope, just like we usually do with threads (threads are always global), we can launch coroutines in the specific scope of the operation we are performing.

In our example, we have main function that is turned into a coroutine using runBlocking coroutine builder. Every coroutine builder, including runBlocking, adds an instance of CoroutineScope to the scope of its code block. We can launch coroutines in this scope without having to join them explicitly, because an outer coroutine (runBlocking in our example) does not complete until all the coroutines launched in its scope complete. Thus, we can make our example simpler:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine in the scope of runBlocking
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

You can get full code here.

Scope builder

In addition to the coroutine scope provided by different builders, it is possible to declare your own scope using coroutineScope builder. It creates a coroutine scope and does not complete until all launched children complete. The main difference between runBlocking and coroutineScope is that the latter does not block the current thread while waiting for all children to complete.

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // Creates a coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // This line will be printed before the nested launch
    }
    
    println("Coroutine scope is over") // This line is not printed until the nested launch completes
}

You can get full code here.

Extract function refactoring

Let's extract the block of code inside launch { ... } into a separate function. When you perform "Extract function" refactoring on this code you get a new function with suspend modifier. That is your first suspending function. Suspending functions can be used inside coroutines just like regular functions, but their additional feature is that they can, in turn, use other suspending functions, like delay in this example, to suspend execution of a coroutine.

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}

You can get full code here.

But what if the extracted function contains a coroutine builder which is invoked on the current scope? In this case suspend modifier on the extracted function is not enough. Making doWorld an extension method on CoroutineScope is one of the solutions, but it may not always be applicable as it does not make API clearer. The idiomatic solution is to have either an explicit CoroutineScope as a field in a class containing the target function or an implicit one when the outer class implements CoroutineScope. As a last resort, CoroutineScope(coroutineContext) can be used, but such approach is structurally unsafe because you no longer have control on the scope of execution of this method. Only private APIs can use this builder.

Coroutines ARE light-weight

Run the following code:

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(1000L)
            print(".")
        }
    }
}

You can get full code here.

It launches 100K coroutines and, after a second, each coroutine prints a dot. Now, try that with threads. What would happen? (Most likely your code will produce some sort of out-of-memory error)

Global coroutines are like daemon threads

The following code launches a long-running coroutine in GlobalScope that prints "I'm sleeping" twice a second and then returns from the main function after some delay:

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // just quit after delay
//sampleEnd    
}

You can get full code here.

You can run and see that it prints three lines and terminates:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...

Active coroutines that were launched in GlobalScope do not keep the process alive. They are like daemon threads.