Coroutines Masterclass: Chapter-2-What are coroutines builders?, launch, async,runblocking…

SUMIT KUMAR
9 min readJul 30, 2023

--

Welcome to the comprehensive guide on Kotlin Coroutines! In this article series, I will take you on a captivating journey, starting from the fundamentals and gradually delving into advanced concepts. Discover the world of essential coroutine builders, from runBlocking to produce.So, grab your favorite cup of coffee, get comfortable, and let’s deep dive into the world of Coroutines.

In this second chapter of our Coroutines Masterclass, we will explore the essential coroutine builders in Kotlin: launch, async, runBlocking, withContext, and produce. These builders simplify the creation and management of coroutines, making asynchronous programming more straightforward and efficient. Before diving into each builder, we'll understand the significance of scopes in Kotlin coroutines, ensuring proper lifecycle management. Get ready to harness the power of these coroutine builders for seamless asynchronous programming! Let's begin the exploration!

“Coroutines masterclass series links are mentioned at the end”

TOPICS COVERED IN THIS CHAPTER

1. Introduction

  • Purpose of Coroutine Builders
  • Overview of Coroutine Builders
  • Importance of Scopes in Kotlin Coroutines

2. runBlocking Coroutine Builder

3. launch Coroutine Builder

4. async Coroutine Builder

5. withContext Coroutine Builder

6. produce Coroutine Builder

7. Comparison between async and launch

8. Summary and Conclusion

An Introduction to Coroutine Builders in Kotlin

Coroutines are a powerful feature in Kotlin that allow us to write asynchronous code in a more straightforward and sequential manner, I hope you have already gone through Coroutines Masterclass: Chapter-1: Foundation of Coroutines. One of the key elements in working with coroutines is using “coroutine builders.

Coroutine builders are functions or constructs that help create and manage coroutines, enabling developers to harness the full potential of asynchronous programming.

Coroutine builders abstract away the complexities of coroutine creation, suspension, and resumption, allowing developers to focus on the logic of their asynchronous tasks. These builders typically provide simple and expressive APIs to start, run, and await the completion of coroutines, making asynchronous programming more readable and maintainable.

Types of coroutines builders we have

In Kotlin, coroutines provide several builders that aid in creating and managing asynchronous tasks. These builders simplify the process of working with coroutines, making asynchronous programming more straightforward and efficient. Below, I am mentioning some commonly used coroutines builder we use in kotlin:

  1. launch
  2. async
  3. runblocking
  4. withContext
  5. produce

“we will know about each of them one by one with their implementation, but before that, we need to know about the role of scope while creating any coroutine. However scope in itself is a wide topic to cover, here I will give you a good idea about it and its significance.”

Understanding the Role of Scopes in Kotlin Coroutine

Scopes in Kotlin coroutines represent a structured environment that manages the lifecycle and execution of coroutines. They define where a coroutine runs, how long it lasts, and when it gets canceled. Scopes are crucial for proper resource management and avoiding issues like memory leaks, ensuring efficient asynchronous programming.

Types of scopes available in Kotlin coroutines:

  1. GlobalScope
  2. CoroutineScope (Custom scopes tied to specific lifecycles or components)
  3. viewModelScope (Specific to Android development, designed for use within ViewModel classes)

Now, note:

“If you don't mention any scope then,GlobalScope is available as the default choice, it should be used with caution due to its unrestricted lifecycle. Coroutines launched in this scope can outlive their intended context, causing unintended consequences. The preferred approach is to use CoroutineScope, which offers more control and safety by allowing developers to create custom scopes tied to specific contexts or components.”

We will talk about scopes in detail some other day for now let’s get back to our topic.

1. runBlocking Coroutine Builder (Only for creating the main coroutine):

The runBlocking coroutine builder is used to create a new coroutine in the main function or unit tests. It blocks the current thread until all the coroutines inside it are completed. It is not used within other coroutines because it is designed to bridge between regular synchronous code and suspending code in the context of the main function.

Advantage:

  • Synchronous testing: runBlocking is primarily used in test code, examples, and main functions to create a new coroutine and block the current thread until its completion. It's a convenient way to write synchronous-style tests for asynchronous code, making testing easier.

Disadvantage:

  • Blocking nature: One significant disadvantage of runBlocking is that it blocks the current thread until the enclosed coroutine completes its execution. This goes against the main idea of non-blocking coroutines and can lead to performance issues if used inappropriately in production code.

Restrictions:

  • Use in non-production scenarios: While runBlocking is useful for testing and some specific situations, it should generally be avoided in production code. Using it extensively could lead to performance problems, defeating the purpose of using coroutines for asynchronous tasks

Implementation:

import kotlinx.coroutines.*

fun main() = runBlocking {
println("Start")
// Using runBlocking to create a coroutine
launch {
delay(1000)
println("Coroutine 1 completed")
}
launch {
delay(2000)
println("Coroutine 2 completed")
}
println("Run blocking completed")
}
Output:

Start
Coroutine 1 completed
Coroutine 2 completed
Run blocking completed

2. launch Coroutine Builder

The launch coroutine builder is used to start a new coroutine that performs a task asynchronously. It is ideal for fire-and-forget scenarios when you need to execute a coroutine without waiting for its result. The launch function returns a Job object, which can be used to manage and cancel the coroutine if necessary.

Advantage:

  • Simplicity: The launch coroutine builder is the simplest way to start a new coroutine. It allows you to quickly fire off a new concurrent task without blocking the current thread.
  • Lightweight: Since it doesn’t return any result (Job object returned by launch doesn't hold any value), it is lightweight and suitable for scenarios where you don't need to retrieve any result from the coroutine.

Disadvantage:

  • No direct result: One of the main disadvantages of launch is that it doesn't provide a way to directly retrieve the result of the coroutine. If you need to get the result from a coroutine, you'll have to resort to other techniques like using shared mutable state or channels for communication.

Restrictions:

  • None: There are no specific restrictions associated with the launch coroutine builder. You can use it almost anywhere in your code where you need to perform concurrent tasks without blocking the main thread.

Implementation:

import kotlinx.coroutines.*

fun main() {
println("Start")
GlobalScope.launch {
delay(1000)
println("Coroutine completed")
}
println("End")
Thread.sleep(2000)
}
Output:

Start
End
Coroutine completed

3. async Coroutine Builder

The async coroutine builder is used to start a new coroutine that performs a computation asynchronously and returns a Deferred result. A Deferred is similar to a Future in other programming languages, and it represents a promise to deliver a result in the future. You can use await() on a Deferred to retrieve the computed result when needed.

Advantage:

  • Asynchronous result: The primary advantage of the async coroutine builder is that it allows you to start a coroutine that computes a result and returns it as a Deferred object. This Deferred acts as a lightweight non-blocking future representing the result of the coroutine.
  • Composition: The use of async allows you to compose multiple asynchronous tasks and retrieve their results individually when needed.

Disadvantage:

  • Delayed execution: Unlike launch, async doesn't start the coroutine immediately. It only starts the coroutine when you explicitly call await() on the associated Deferred object. This introduces a level of complexity and might not be suitable for scenarios where immediate execution is required.

Restrictions:

  • None: There are no specific restrictions associated with the async coroutine builder. However, it's important to remember that async is most effective when used in combination with await() to retrieve results and combine asynchronous tasks.

Implementation:

import kotlinx.coroutines.*

fun main() = runBlocking {
val deferredResult = async {
delay(1000)
"Async coroutine result"
}
println("Do something else while waiting...")
val result = deferredResult.await()
println("Result: $result")
}
Output:

Do something else while waiting...
Result: Async coroutine result

4. withContext Coroutine Builder:

The withContext coroutine builder is used to switch the context of a coroutine to a different thread or coroutine dispatcher. It is often used when you need to perform a specific task in a different execution context, such as switching from the main thread to the background thread or vice versa.

Advantage:

  • Context switching: The withContext coroutine builder allows you to switch the context of a coroutine to a different dispatcher. This can be beneficial for performing work on a specific thread or thread pool. It's commonly used to perform operations that should not block the main (UI) thread.

Disadvantage:

  • No new coroutine creation: Unlike launch or async, withContext doesn't create a new coroutine. Instead, it changes the context of an existing coroutine. This means it cannot be used to launch new independent tasks, and it's generally used to switch the execution context within a coroutine.

Restrictions:

  • Use within a coroutine: withContext is designed to be used within other coroutine functions. Attempting to use it outside of a coroutine will result in a compilation error

Implementation:

import kotlinx.coroutines.*

fun main() = runBlocking {
println("Start")
// Using withContext to switch the coroutine context
withContext(Dispatchers.Default) {
println("Running on ${Thread.currentThread().name}")
}
println("Back to ${Thread.currentThread().name}")
}
Output:

Start
Running on DefaultDispatcher-worker-1
Back to main

5. produce Coroutine Builder:

The produce coroutine builder is used to create a coroutine that produces a stream of elements. It is similar to a generator or an iterator in other programming languages, allowing you to emit values from the coroutine and consume them using a receive() function.

Advantage:

  • Producer-consumer pattern: produce and actor are used for creating channels and performing the producer-consumer pattern in coroutines. This allows different parts of the application to communicate efficiently, passing data between coroutines in a non-blocking manner.

Disadvantage:

  • Complexity: Working with channels and the producer-consumer pattern can introduce more complexity into your code. It requires careful consideration and proper handling to avoid potential bugs, deadlocks, or race conditions.

Restrictions:

  • Deprecation of actor: As of Kotlin 1.5.0, the actor coroutine builder is deprecated and should be avoided in favor of other constructs like channels, as it had some limitations and issues.

Implementation:

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*

fun main() = runBlocking {
val channel = produce {
for (i in 1..5) {
delay(500)
send(i)
}
}
for (element in channel) {
println("Received: $element")
}
}
Output:

Received: 1
Received: 2
Received: 3
Received: 4
Received: 5

Difference between async and launch in short.

| Aspect                 |   async                                    | `launch`                                 |
|------------------------|--------------------------------------------|------------------------------------------|
| Return Type | `Deferred<T>` | `Job` |
| Purpose | Asynchronous computation with a result | Asynchronous tasks |
| Result Retrieval | `await()` to retrieve the computed result | No direct result retrieval |
| Use Case | Computation tasks with a result | Fire-and-forget tasks |
| Requires `runBlocking` | Yes (to use `await()`) | No (returns `Job` immediately) |
| Error Handling | Propagated to `await()` | Exceptions must be handled |
| Cancellation | Propagated to `await()` | Can be manually canceled |
| Exception Aggregation | Exceptions from multiple `async` calls | Exceptions are separate for each `launch`|
| Multiple Results | Can return multiple `Deferred` results | Each `launch` has its own `Job` instance |
| Parent Job | Inherits parent's job hierarchy | Inherits parent's job hierarchy |
| Return Value | `async` returns a single result | `launch` does not return a specific value|
| Parallel Execution | Suitable for parallel computation tasks | Not directly optimized for parallel tasks|
| CoroutineScope | Can be used with a custom CoroutineScope | Often used with `GlobalScope` |

Summary:

In this chapter, we explored the fundamental concepts of coroutine builders in Kotlin, which play a crucial role in asynchronous programming. Coroutine builders are functions or constructs that simplify the creation and management of coroutines, making asynchronous code more readable and maintainable. The key coroutine builders we discussed are launch, async, runBlocking, withContext, and produce.

  • launch is used to start a new coroutine for fire-and-forget scenarios, returning a Job object for managing the coroutine's lifecycle.
  • async is used for computations that return a result in the future, returning a Deferred that holds the result when awaited.
  • runBlocking is specifically for the main function or unit tests, blocking the current thread until all coroutines inside are completed.
  • withContext switches the context of a coroutine to a different dispatcher, useful for executing tasks in different execution contexts.
  • produce creates coroutines that produce a stream of elements, allowing for convenient data streaming.

Understanding the role of scopes in coroutines is vital. While the GlobalScope is available as a default, it should be used cautiously due to its unrestricted lifecycle. Instead, it is preferred to use a CoroutineScope for better control and safety, creating custom scopes tied to specific contexts or components.

Thank you for reading! I hope you enjoyed the article and found it valuable in enhancing your knowledge. This was (chapter-2), and there’s much more to explore in the upcoming chapters of the series. Stay tuned for high-quality articles that will further enrich your understanding. If you have any further questions or would like to contribute to the discussion, please feel free to leave your comments below. Your feedback and engagement are greatly appreciated. Thank you once again, and happy coding!

INIT KOTLIN : )

--

--

SUMIT KUMAR
SUMIT KUMAR

Written by SUMIT KUMAR

SDE(Android) || Ex-Ingenium || Modern Mobile Developer📱

Responses (1)