Reader small image

You're reading from  Kotlin Design Patterns and Best Practices - Third Edition

Product typeBook
Published inApr 2024
PublisherPackt
ISBN-139781805127765
Edition3rd Edition
Right arrow
Author (1)
Alexey Soshin
Alexey Soshin
author image
Alexey Soshin

Alexey Soshin is a software architect with 18 years of experience in the industry. He started exploring Kotlin when Kotlin was still in beta, and since then has been a big enthusiast of the language. He's a conference speaker, published writer, and the author of a video course titled Pragmatic System Design
Read more about Alexey Soshin

Right arrow

Threads and Coroutines

This chapter promises to be an exciting one as we take a deeper dive into the realm of concurrency in Kotlin. You may recall that in the previous chapter, we touched upon how our application could efficiently handle thousands of requests per second. To illustrate the importance of immutability, we introduced you to the concept of a race condition using two threads.

In this chapter, we’ll extend that understanding and explore the following:

  • Looking deeper into threads: How do threads work in Kotlin and what are the advantages and disadvantages of using them?
  • Introducing coroutines: What are coroutines and how do suspend functions facilitate them?
  • Starting coroutines: How do you launch a new coroutine and what are the different ways to do it?
  • Jobs: Understand what jobs are in the context of coroutines and how they help manage concurrent operations.
  • Coroutines under the hood: How does the Kotlin compiler handle coroutines...

Technical requirements

There are no additional requirements compared to the previous chapter. You can find the source code for this chapter here: https://github.com/PacktPublishing/Kotlin-Design-Patterns-and-Best-Practices_Third-Edition/tree/main/Chapter06.

Looking deeper into threads

Before delving into the technical details, let’s first understand what problems threads are designed to solve.

Modern computers and smartphones today are commonly equipped with multi-core CPUs. This architecture enables the computer to perform multiple tasks in parallel. This is a dramatic improvement compared to 15 years ago, when single-core CPUs were the norm, and dual-core CPUs were a luxury for tech enthusiasts.

However, even with older, single-core CPUs, you weren’t limited to performing just one task at a time. You could listen to music while browsing the web, for example. How is that possible? The CPU employs a task-switching strategy, much like your brain does when multitasking. When you’re reading a book and listening to someone talk at the same time, your attention is divided between the two activities, switching back and forth.

Although modern CPUs can handle multiple requests simultaneously, consider a scenario...

Introducing coroutines

Kotlin introduces coroutines alongside Java’s threading model, offering a lightweight alternative. These coroutines come with various advantages over traditional threads. They are more efficient, reducing resource consumption by enabling efficient multiplexing. Multiplexing, in the context of coroutines, refers to the ability to handle multiple tasks concurrently within a single thread. Unlike traditional threading, where each task might require a separate thread, multiplexing allows these tasks to share threads more efficiently. This is achieved through the suspension and resumption of coroutine executions.

Coroutines follow structured concurrency, simplifying task management and preventing resource leaks. They support suspending and resuming execution, ideal for non-blocking IO operations, and enhance code responsiveness.

Additionally, they offer built-in cancelation and streamlined error handling, making code more predictable. Coroutines make...

Jobs

The result of running an asynchronous task is referred to as a job. Just as the Thread object represents an actual OS thread, the Job object represents an actual coroutine.

For instance, consider the following function that initiates a coroutine to generate a universally unique identifier (UUID) asynchronously and returns it:

fun fastUuidAsync() = GlobalScope.async { 
    UUID.randomUUID() 
}

However, if we execute this code from our main method, it won’t print the expected UUID value. Instead, it will produce a result similar to the following:

> DeferredCoroutine{Active} 

The object returned from a coroutine is known as a job. Now, let’s explore what a job is and how to use it correctly.

To illustrate this concept, consider the following code snippet:

fun main() {
    runBlocking {
        val job: Deferred<UUID> = fastUuidAsync()
        println(job.await())
    }
}

A job has a simple life cycle and can be in one of...

Coroutines under the hood

We’ve highlighted some key facts about coroutines several times:

  • Coroutines are akin to lightweight threads. They consume fewer resources compared to regular threads, enabling the creation of more concurrent tasks.
  • Unlike traditional threads that block the entire thread when waiting for an operation to complete, coroutines suspend themselves, allowing the underlying thread to execute other tasks in the meantime.

But how exactly do coroutines work? Let’s explore the mechanics using an example:

class Blocking {
    companion object {
        fun profile(id: String): Profile {
            val bio = fetchBioOverHttp(id) // takes 1s
            val picture = fetchPictureFromDB(id) // takes 100ms
            val friends = fetchFriendsFromDB(id) // takes 500ms
            return Profile(bio, picture, friends)
        }
 
        private fun fetchFriendsFromDB(id: String): List<String> {
            Thread.sleep(500...

Dispatchers

In the section discussing the high cost of threads, we touched on the concept of executors in Java. Previously, we utilized a coroutine scope for writing asynchronous code. Now, we will examine the rationale behind the use of coroutine dispatchers in Kotlin.

When we ran our coroutines using the runBlocking function, their code was executed on the main thread.

You can check this by running the following code:

fun main() {
    runBlocking {
        launch {
            println(Thread.currentThread().name)
        }
    }
}

This prints the following output:

> main

In contrast, when we run a coroutine using GlobalScope, it runs on something called DefaultDispatcher:

fun main() {
    runBlocking {
        GlobalScope.launch {
            println("GlobalScope.launch: ${Thread.currentThread().name}")
        }
    }
}

This prints the following output:

> DefaultDispatcher-worker-1

DefaultDispatcher is a thread pool that...

Structured concurrency

Structured concurrency is a concept in Kotlin that ensures the orderly execution and completion of coroutines. It ties the life cycle of coroutines to the scope they are launched in, making it easier to manage and control them. Under structured concurrency, when a coroutine scope is canceled or completes its execution, all coroutines launched within that scope are also canceled or completed. This approach simplifies the handling of concurrent operations, preventing resource leaks and ensuring that coroutines don’t run longer than necessary.

Let’s look at an example. It is a very common practice to spawn coroutines from inside another coroutine.

The first rule of structured concurrency is that the parent coroutine should always wait for all its children to complete. This prevents resource leaks, which are very common in languages that don’t have the structured concurrency concept.

This means that if we look at the following code...

Summary

In this chapter, we’ve explored the creation of threads and coroutines in Kotlin, highlighting the advantages of coroutines over traditional threads. While Kotlin offers a simpler syntax for thread creation compared to Java, it still comes with memory and performance overheads. Coroutines offer an efficient alternative for concurrent code execution in Kotlin.

By this point, you should be well versed in initiating and awaiting the completion of coroutines, as well as in retrieving their results. Additionally, we have covered the structured nature of coroutines and their interaction with dispatchers.

Furthermore, we’ve introduced the concept of structured concurrency, a contemporary approach that simplifies the prevention of resource leaks in concurrent code.

In the next chapter, we’ll explore how to leverage these concurrency mechanisms to design scalable and robust systems tailored to our requirements.

Questions

  1. What are the different ways to start a coroutine in Kotlin?
  2. With structured concurrency, if one of the coroutines fails, all the siblings will be canceled as well. How can we prevent that behavior?
  3. What is the purpose of the yield() function?

Learn more on Discord

Join our community’s Discord space for discussions with the author and other readers:

https://discord.com/invite/xQ7vVN4XSc

lock icon
The rest of the chapter is locked
You have been reading a chapter from
Kotlin Design Patterns and Best Practices - Third Edition
Published in: Apr 2024Publisher: PacktISBN-13: 9781805127765
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
undefined
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime

Author (1)

author image
Alexey Soshin

Alexey Soshin is a software architect with 18 years of experience in the industry. He started exploring Kotlin when Kotlin was still in beta, and since then has been a big enthusiast of the language. He's a conference speaker, published writer, and the author of a video course titled Pragmatic System Design
Read more about Alexey Soshin