Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Kotlin Coroutines: Fundamentals, Cancellation, and Timeout Handling

Tech 2

Asynchronous programmming in Kotlin relies on suspend functions, utilizing builders like launch and async from the kotlinx.coroutines library.

Initiating a Coroutine

A coroutine serves as a lightweight thread. It is launched within a specific CoroutineScope using a builder such as launch. When started in GlobalScope, the coroutine's lifespan is tied to the entire application.

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        delay(1500L) // Non-blocking suspension
        println("Universe!")
    }
    println("Greetings,")
    Thread.sleep(3000L) // Block main thread to keep JVM alive
}

Output:

Greetings, Universe!

Replacing GlobalScope.launch with thread and delay with Thread.sleep achieves a similar outcome. However, invoking a suspend function like delay outside a coroutine results in a compilation error, as suspend functions are restricted to coroutine contexts.

Bridging Blocking and Non-Blocking Contexts

Mixing blocking (Thread.sleep) and non-blocking (delay) calls can cause confusion. The runBlocking builder provides a clear boundary, blocking the current thread until its execution block completes.

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    GlobalScope.launch {
        delay(1500L)
        println("Universe!")
    }
    println("Greetings,")
    delay(3000L)
}

Instead of delaying arbitrarily to wait for a background task, storing the Job returned by launch and calling join() ensures the main coroutine waits for the child coroutine to finish.

Structured Concurrency

Relying on GlobalScope creates unmanaged top-level coroutines that consume memory even if forgotten. Structured concurrency solves this by launching coroutines within specific scopes. The outer coroutine (runBlocking in this case) will not complete until all launched child coroutines finish, eliminating the need for explicit join() calls.

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1500L)
        println("Universe!")
    }
    println("Greetings,")
}

Custom Scope Builders

The coroutineScope builder declares a custom scope that suspends until all child coroutines complete. Unlike runBlocking, which blocks the underlying thread, coroutineScope merely suspends, freeing the thread for other tasks.

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(300L)
        println("Outer scope task")
    }
    coroutineScope {
        launch {
            delay(700L)
            println("Inner scope task")
        }
        delay(150L)
        println("Coroutine scope message")
    }
    println("Scope completed")
}

Extracting Suspend Functions

Refactoring a coroutine block into a separate function requires adding the suspend modifier. This allows the function to invoke other suspend functions freely.

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { executeTask() }
    println("Greetings,")
}

suspend fun executeTask() {
    delay(1500L)
    println("Universe!")
}

If the extracted function needs to launch a coroutine, passing a CoroutineScope as a receiver or parameter is the idiomatic approach.

Daemon-like Behavior of Global Coroutines

Coroutines launched in GlobalScope do not prevent the JVM from shutting down, functioning similarly to daemon threads.

import kotlinx.coroutines.*

fun main() = runBlocking {
    GlobalScope.launch {
        repeat(1000) { idx ->
            println("Working on iteration $idx...")
            delay(600L)
        }
    }
    delay(2000L)
}

Terminating Coroutines

Long-running background tasks often require programmatic cancellation. The Job object provides cancel() and join() methods. The combined cancelAndJoin() function is typically used to halt a coroutine and wait for its finalization.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val backgroundTask = launch {
        repeat(1000) { idx ->
            println("Task running: iteration $idx...")
            delay(600L)
        }
    }
    delay(2000L)
    println("Initiating cancellation...")
    backgroundTask.cancelAndJoin()
    println("Task terminated.")
}

Cooperative Cancellation

Coroutine cencellation is cooperative; simply calling cancel does not forcefully stop execution. While standard suspend functions like delay check for cancellation and throw CancellationException, a tightly bound computation loop will ignore cancellation requests.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val computationTask = launch(Dispatchers.Default) {
        var counter = 0
        var nextTimestamp = System.currentTimeMillis()
        while (counter < 10) {
            if (System.currentTimeMillis() >= nextTimestamp) {
                println("Computing: step ${counter++}")
                nextTimestamp += 500L
            }
        }
    }
    delay(1500L)
    println("Attempting cancellation...")
    computationTask.cancelAndJoin()
    println("Finished.")
}

Enforcing Cancellation in Computation

To make computation logic responsive to cancellation, check the isActive property within the loop condition, or periodically invoke a suspand function like yield().

import kotlinx.coroutines.*

fun main() = runBlocking {
    val cancellableTask = launch(Dispatchers.Default) {
        var counter = 0
        var nextTimestamp = System.currentTimeMillis()
        while (isActive) {
            if (System.currentTimeMillis() >= nextTimestamp) {
                println("Computing: step ${counter++}")
                nextTimestamp += 500L
n            }
        }
    }
    delay(1500L)
    println("Initiating cancellation...")
    cancellableTask.cancelAndJoin()
    println("Task successfully halted.")
}

Resource Cleanup with Finally

When a cancellable suspend function throws CancellationException, standard try-finally blocks or Kotlin's use function execute their cleanup logic as expected.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val resourceTask = launch {
        try {
            repeat(1000) { idx ->
                println("Allocating step $idx...")
                delay(500L)
            }
        } finally {
            println("Releasing resources in finally block")
        }
    }
    delay(1500L)
    println("Triggering stop...")
    resourceTask.cancelAndJoin()
    println("Cleanup complete.")
}

Non-Cancellable Execution Blocks

Invoking a suspend function inside a finally block usually fails with CancellationException because the coroutine is already cancelled. For cleanup operations requiring suspension (like closing a database connection asynchronously), wrap the logic in withContext(NonCancellable).

import kotlinx.coroutines.*

fun main() = runBlocking {
    val resilientTask = launch {
        try {
            repeat(1000) { idx ->
                println("Executing step $idx...")
                delay(400L)
            }
        } finally {
            withContext(NonCancellable) {
                println("Starting non-cancellable cleanup")
                delay(1000L)
                println("Cleanup finalized after delay")
            }
        }
    }
    delay(1000L)
    println("Halting task...")
    resilientTask.cancelAndJoin()
    println("Process ended.")
}

Handling Timeouts

Manually tracking a job to cancel it after a duration is unnecessary; withTimeout handles this directly. It throws TimeoutCancellationException (a subclass of CancellationException) upon expiration.

import kotlinx.coroutines.*

fun main() = runBlocking {
    withTimeout(1500L) {
        repeat(1000) { idx ->
            println("Processing item $idx...")
            delay(400L)
        }
    }
}

To treat a timeout as a null result rather than an exception, use withTimeoutOrNull.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val outcome = withTimeoutOrNull(1500L) {
        repeat(1000) { idx ->
            println("Processing item $idx...")
            delay(400L)
        }
        "Completed successfully"
    }
    println("Operation result: $outcome")
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.