ICC- Inter-Coroutine Communication in Kotlin
Amit Nadiger
Polyglot(Rust??, Move, C++, C, Kotlin, Java) Blockchain, Polkadot, UTXO, Substrate, Sui, Aptos, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Cas, Engineering management.
Kotlin Coroutines are a powerful tool for asynchronous programming in Kotlin. They allow developers to write asynchronous code in a synchronous style, making it easier to reason about and maintain. One of the most important aspects of asynchronous programming is communication between threads or tasks. In this article, we will discuss how Kotlin coroutines can communicate with each other and with the outside world.
Communication between Coroutines
Coroutines can communicate with each other in several ways, including channels, actors, and shared mutable state,etc.
Let me try to explain one by one.
Here are some of the APIs provided by the Channel class in Kotlin coroutines:
These are some of the most commonly used APIs of the Channel class, but there are many more available for handling different scenarios and use cases
Example:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (i in 1..5) channel.send(i * i)
channel.close()
}
repeat(5) { println(channel.receive()) }
println("Done!")
}
/*
O/P =>
1
4
9
16
25
Done!
*/
Advantages:
Disadvantages:
Suitable scenarios:
2. Shared mutable state: Coroutines can also communicate through shared mutable state, such as variables or objects. However, this can be risky and lead to race conditions if not properly synchronized. To avoid race conditions, Kotlin provides a set of thread-safe data structures, such as ConcurrentHashMap and AtomicReference, that can be used to safely share data between coroutines.
=> ConcurrentHashMap can also be used for communication between coroutines in Kotlin. It is a thread-safe implementation of java.util.Map interface, which means it can be safely accessed by multiple threads (or coroutines in this case) without the need for external synchronization.
import kotlinx.coroutines.*
import java.util.concurrent.ConcurrentHashMap
val sharedData = ConcurrentHashMap<String, String>()
fun main() = runBlocking<Unit> {
launch {
// Coroutine 1: Write some data to the shared map
sharedData["key"] = "Hello from Coroutine 1!"
println("Coroutine 1: Data written to map")
// Simulate some work before accessing the shared data again
delay(1000)
// Retrieve data written by Coroutine 2
val value = sharedData["key2"]
println("Coroutine 1: Retrieved value '$value' from map")
}
launch {
// Coroutine 2: Write some data to the shared map
sharedData["key2"] = "Hello from Coroutine 2!"
println("Coroutine 2: Data written to map")
// Simulate some work before accessing the shared data again
delay(500)
// Retrieve data written by Coroutine 1
val value = sharedData["key"]
println("Coroutine 2: Retrieved value '$value' from map")
}
}
/*
Op =>
Coroutine 1: Data written to map
Coroutine 2: Data written to map
Coroutine 2: Retrieved value 'Hello from Coroutine 1!' from map
Coroutine 1: Retrieved value 'Hello from Coroutine 2!' from map
*/
In this example, we create a ConcurrentHashMap object called sharedData, which is used by two coroutines to exchange data. Each coroutine writes a value to the map under a unique key and then retrieves the value written by the other coroutine. Since ConcurrentHashMap is thread-safe, we do not need any external synchronization or locking mechanisms to protect the shared data
Advantages:
Disadvantages:
Suitable scenarios:
3. Actor: Actors are a higher-level abstraction on top of channels that provide a way to encapsulate state and behavior into a single entity. Actors can receive messages and react to them by modifying their internal state or sending messages to other actors. Actors can be created using the actor() function and can have their own coroutine context and dispatcher.
Actors provide a higher-level abstraction than channels, allowing multiple coroutines to communicate with a single entity (the actor) in a thread-safe manner. Actors have a mailbox that holds incoming messages, and they process each message sequentially.
As said earlier actor is also a coroutine. In fact, an actor is a special type of coroutine that provides a way to communicate and synchronize between multiple coroutines in a structured way. An actor is a stateful object that encapsulates some functionality and provides a mailbox-like communication channel to receive messages from other coroutines.
When you create an actor, you create a new coroutine that runs in the background and waits for messages to arrive. When a message arrives, the actor coroutine is resumed, and it can handle the message in some way. The actor can also send messages to other coroutines by suspending itself and waiting for a response.
Since actors are implemented as coroutines, they also have a coroutine context that defines their execution context. This context includes things like the dispatcher that determines which thread or thread pool the actor runs on, as well as other context elements like exception handlers, debug information, and so on.
Please note that the Actor class is not part of the standard Kotlin library, but there are libraries like kotlinx.coroutines that provide an Actor implementation. The Actor in kotlinx.coroutines is an implementation of the actor model for concurrent and distributed computing, where actors are independent units of computation that communicate through message passing.
The kotlinx.coroutines Actor class provides the following APIs:
These APIs allow for creating and controlling actors, as well as sending messages to them.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.actor
sealed class Message // Sealed class to represent the messages that can be sent to the Actor
data class Add(val x: Int, val y: Int) : Message() // Message to add two numbers
data class Subtract(val x: Int, val y: Int) : Message() // Message to subtract two numbers
suspend fun calculatorActor() = CoroutineScope(Dispatchers.Default).actor<Message>(Dispatchers.Default) {
? ? for (msg in channel) {
? ? ? ? when (msg) {
? ? ? ? ? ? is Add -> println("${msg.x} + ${msg.y} = ${msg.x + msg.y}")
? ? ? ? ? ? is Subtract -> println("${msg.x} - ${msg.y} = ${msg.x - msg.y}")
? ? ? ? }
? ? }
}
fun main() = runBlocking<Unit> {
? ? val calculator = calculatorActor()
? ? // Launch two coroutines that send messages to the calculator actor
? ? launch { calculator.send(Add(2, 3)) }
? ? async { calculator.send(Subtract(10, 5)) }
? ? delay(1000) // Wait for the messages to be processed
? ? calculator.close() // Close the calculator actor
}
/*
O/p =>
2 + 3 = 5
10 - 5 = 5
*/
In the above example, we create an actor using the actor builder function. The actor processes messages of two types: Add and Subtract. When it receives an Add message, it adds the two numbers and prints the result. When it receives a Subtract message, it subtracts the second number from the first and prints the result.
We then launch two coroutines that send messages to the calculator actor. One sends an Add message, and the other sends a Subtract message. Finally, we wait for a second to give the actor time to process the messages, then close the calculator actor.
So, in this example, the two coroutines that are communicating are the ones that send messages to the calculator actor. The actor itself is not a coroutine, but it uses coroutines internally to process messages asynchronously.
Please note that the Actor class is not part of the standard Kotlin library, but there are libraries like kotlinx.coroutines that provide an Actor implementation.
Advantages:
Disadvantages:
Suitable scenarios:
4. Broadcast Channels: Broadcast channels are similar to regular channels, but they allow multiple consumers to receive the same message. They can be used for one-to-many communication, where a single producer coroutine sends messages to multiple consumer coroutines.
BroadcastChannel is a Kotlin coroutine-based implementation of the publish-subscribe pattern. It provides the following APIs:
Example:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
suspend fun producer(channel: BroadcastChannel<String>) {
// Sends 3 messages to the channel with a delay of 1 second between each send
repeat(3) {
delay(1000)
channel.send("Message #$it")
}
// Closes the channel when done sending messages
channel.close()
}
fun main() = runBlocking<Unit> {
val channel = BroadcastChannel<String>(1) // Creates a BroadcastChannel with a buffer size of 1
launch { // Launches a coroutine as a consumer
channel.consumeEach { println("Consumer 1 received: $it") }
}
launch { // Launches another coroutine as a consumer
channel.consumeEach { println("Consumer 2 received: $it") }
}
producer(channel) // Launches a coroutine as a producer
}
/*
Op =>
Consumer 1 received: Message #0
Consumer 2 received: Message #0
Consumer 1 received: Message #1
Consumer 2 received: Message #1
Consumer 1 received: Message #2
Consumer 2 received: Message #2
*/
In the above example, we create a BroadcastChannel with a buffer size of 1, which means that the channel can hold one message before blocking. We launch two coroutines as consumers using the consumeEach method, which suspends the coroutine until a message is received from the channel. We also launch a coroutine as a producer, which sends three messages to the channel with a delay of 1 second between each send. Since we are using a BroadcastChannel, both consumer coroutines receive all three messages.
What is difference between channel and BroadcastChannel :
Channel is designed for point-to-point communication between a single sender and a single receiver, while a BroadcastChannel is designed for one-to-many communication, where multiple receivers can subscribe to the same channel and receive messages independently.
Channel is a unidirectional channel which can be used to send and receive elements between a sender and a receiver coroutine. Only one coroutine can send data to the channel at a time, and only one coroutine can receive data from the channel at a time. The channel can have multiple senders or receivers, but each sender or receiver will have its own separate channel object.
On the other hand, BroadcastChannel is a multi-cast channel which can have multiple subscribers that receive elements sent to the channel. When a value is sent to a broadcast channel, all the subscribers will receive it. It is possible to have multiple senders, but only one sender can send a value at a time.
The main difference between Channel and BroadcastChannel is that Channel is unidirectional and can have only one sender and one receiver, while BroadcastChannel is multi-cast and can have multiple subscribers.
However, the difference between Channel and BroadcastChannel becomes more relevant when there are multiple subscribers. In the case of Channel, each subscriber would need its own separate Channel object in order to receive the data. This can become unwieldy if there are many subscribers.
In contrast, BroadcastChannel provides a way to easily send data to multiple subscribers using a single channel object. This can be useful in scenarios such as sending real-time updates to multiple clients over a network.
Additionally, BroadcastChannel has some features that are not available in Channel, such as the ability to specify a backpressure strategy to control how the channel handles overproducing or under consuming subscribers.
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking<Unit> {
val channel = Channel<Int>()
val broadcastChannel = BroadcastChannel<Int>(1)
// Send values to the channels
launch {
for (i in 1..5) {
channel.send(i)
broadcastChannel.send(i)
}
channel.close()
broadcastChannel.close()
}
// Receive values from the channels
launch {
println("Channel:")
for (value in channel) {
delay(100) // simulate some processing time
println("Received: $value")
}
println("\nBroadcastChannel:")
/*
Below line creates a new ReceiveChannel that can be used
to receive values from the BroadcastChannel.
*/
val subscription = broadcastChannel.openSubscription()
// use the subscription
subscription.consumeEach {
//delay(100) // simulate some processing time
println("Received message: $it")
}
// close the subscription manually
subscription.cancel()
}
}
/*
Channel:
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
BroadcastChannel:
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
*/
In the above example, we create both a Channel and a BroadcastChannel. We then send values to both channels in a loop and close the channels when we're done.
broadcastChannel.openSubscription() creates a new ReceiveChannel that can be used to receive values from the BroadcastChannel. In other words, it opens a subscription to the BroadcastChannel, allowing a coroutine to start receiving values that are sent to the channel. The subscription returned by openSubscription() can be used like any other ReceiveChannel to receive the values sent by the BroadcastChannel.
Next, we have a coroutine that receives values from the Channel and another coroutine that receives values from the BroadcastChannel.
Here Channel and BroadcastChannel behave differently:
As we can see, the values sent to the Channel are received one at a time in the order they were sent. However, the values sent to the BroadcastChannel are received all at once when the subscription is opened. This means that multiple coroutines can receive the same values from the BroadcastChannel at the same time.
The reason for the difference in behavior between Channel & BroadcastChannel is not related to buffering. Both Channel & BroadcastChannel can have buffering if you specify it when creating the channel.
The key difference between Channel and BroadcastChannel is in how the values are delivered to the receivers. In a Channel, the values are delivered to the receivers one at a time in the order they were sent, and each value is consumed by only one receiver. This is often referred to as "point-to-point" communication.
In a BroadcastChannel, the values are delivered to all current and future receivers at once when the subscription is opened. This means that multiple coroutines can receive the same values from the BroadcastChannel at the same time. This is often referred to as "publish-subscribe" communication.
领英推荐
BTW do you know what is Backpressure ?
Backpressure is a technique used to handle situations where the producer is generating data faster than the consumer can consume it. In other words, it's a way to regulate the flow of data between two components in order to prevent one from overwhelming the other.
In the context of coroutines, backpressure is often used in conjunction with channels to control the flow of data. By default, channels in Kotlin are unbuffered, which means that the sender will be suspended until the receiver is ready to receive the data. This can lead to a situation where the sender is blocked and unable to make progress, even if the receiver is slow.
To handle this situation, channels can be configured with a buffer size. This allows the sender to continue sending data even if the receiver is not immediately ready to consume it. However, if the buffer becomes full, the sender will be suspended until space is available in the buffer. This is an example of backpressure in action.
In addition to buffer size, channels can also be configured with various backpressure strategies to handle situations where the producer is generating data faster than the consumer can consume it. For example, a common backpressure strategy is to drop the oldest items in the buffer when it becomes full, in order to make room for new items. Another strategy is to suspend the sender until space is available in the buffer, but with a timeout so that the sender doesn't get blocked indefinitely.
5. Mutex: Mutexes are synchronization mechanisms that allow a single coroutine to acquire exclusive access to a shared resource or block of code. They can be used to ensure that only one coroutine can modify a shared resource at a time.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
var counter = 0 // A shared variable that will be modified by multiple coroutines
val mutex = Mutex() // Creates a mutex to protect access to the counter variable
suspend fun increment() {
? ? mutex.withLock {
? ? ? ? val current = counter
? ? ? ? delay(100)
? ? ? ? counter = current + 1
? ? }
}
fun main() = runBlocking<Unit> {
? ? val jobs = List(100) {
? ? ? ? launch {
? ? ? ? ? ? repeat(10) {
? ? ? ? ? ? ? ? increment()
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? jobs.forEach { it.join() }
? ? println("Counter: $counter")
}
// Op => Counter: 1000
In the above example, we create a shared variable called counter that will be modified by multiple coroutines. We also create a mutex to protect access to the counter variable. The increment function is marked with the suspend modifier and acquires the mutex using the withLock function. This ensures that only one coroutine can modify the counter variable at a time. We launch 100 coroutines that each call the increment function 10 times, which increments the counter variable by a total of 1000. We use the join method to wait for all coroutines to finish before printing the final value of the counter variable.
6. Semaphores: Semaphores are similar to mutexes, but they allow a fixed number of coroutines to access a shared resource or block of code simultaneously. They can be used to limit the number of coroutines that can access a shared resource at any given time.
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.*
var counter = 0 // A shared variable that will be modified by multiple coroutines
val semaphore = Semaphore(2) // Creates a semaphore with a limit of 2 coroutines
suspend fun increment() {
semaphore.withPermit {
val current = counter
delay(100)
counter = current + 1
}
}
fun main() = runBlocking<Unit> {
val jobs = List(10) {
launch {
repeat(10) {
increment()
}
}
}
jobs.forEach { it.join() }
println("Counter: $counter")
}
/*
Op => Counter: 100
*/
In this example, we create a shared variable called counter that will be modified by multiple coroutines. We also create a semaphore with a limit of 2 coroutines. The increment function is marked with the suspend modifier and acquires a permit with withPermit method and imbrutements the shared variable. The increment function is a coroutine function that tries to acquire a permit from the semaphore before modifying the counter. The withPermit function suspends the coroutine until a permit is available. Once a permit is acquired, the function reads the current value of counter, adds a delay of 100 milliseconds, and increments the counter by 1.
The increment function is a coroutine function that tries to acquire a permit from the semaphore before modifying the counter. The withPermit function suspends the coroutine until a permit is available. Once a permit is acquired, the function reads the current value of counter, adds a delay of 100 milliseconds, and increments the counter by 1.
7. Locks: Locks are synchronization mechanisms that allow coroutines to acquire exclusive access to a shared resource or block of code. They can be used to ensure that only one coroutine can modify a shared resource at a time, similar to mutexes.
In Kotlin, Mutex and Lock are related but not exactly the same.
Mutex is a type of lock that provides mutual exclusion (hence the name "Mutex") and is implemented using a combination of a Lock and a Condition. It ensures that only one coroutine can hold the lock at a time, and other coroutines will wait until the lock is released before trying to acquire it.
On the other hand, Lock is a more general concept that refers to any mechanism that can be used to prevent concurrent access to a shared resource. In Kotlin, the Lock interface defines a basic set of methods for acquiring and releasing locks, but does not provide any additional functionality beyond that.
In general, Mutex is a more powerful synchronization primitive than Lock, as it provides additional features such as the ability to specify timeouts for acquiring the lock, and the ability to query whether the lock is currently held or not. However, if you only need a basic lock for synchronization purposes, you can use the Lock interface directly.
8. Atomic Variables: Atomic variables are special types of variables that allow for safe access and modification from multiple coroutines. They can be used to ensure that shared data is accessed and modified in a thread-safe manner without requiring explicit synchronization.
import kotlinx.coroutines.*
import java.util.concurrent.atomic.AtomicInteger
val counter = AtomicInteger(0) // An atomic variable that will be modified by multiple coroutines
suspend fun increment() {
delay(100)
counter.incrementAndGet()
}
fun main() = runBlocking<Unit> {
val jobs = List(100) {
launch {
repeat(10) {
increment()
}
}
}
jobs.forEach { it.join() }
println("Counter: ${counter.get()}")
}
// Op -> Counter: 1000
In the above example, we create an atomic variable called counter that will be modified by multiple coroutines. The increment function is marked with the suspend modifier and uses the incrementAndGet method of the atomic variable to increment its value by 1. The atomic variable ensures that the value is incremented atomically and without race conditions, which means that multiple coroutines can modify the variable without interfering with each other. We launch 100 coroutines that each call the increment function 10 times, which increments the counter variable by a total of 1000. We use the join method to wait for all coroutines to finish before printing the final value of the counter variable.
9. Flow: Flow is a reactive stream processing library that allows for asynchronous and non-blocking processing of data streams. It can be used to communicate between coroutines and with the outside world in a reactive and efficient manner.
Here are some of the commonly used APIs of Flow in Kotlin:
What is emit then?
emit() is a suspending function provided by the Kotlin coroutines library to emit elements from a suspending function or a coroutine. It is used with a Flow object to emit values from the flow. When called, emit() sends a value to the downstream collectors of the Flow.
Here's an example:
fun getNumbers(): Flow<Int> = flow {
? ? for (i in 1..10) {
? ? ? ? emit(i)
? ? }
}
fun main() = runBlocking {
? ? getNumbers().collect { value -> println(value) }
}
In the above example, getNumbers() is a Flow that emits values from 1 to 10 using the emit() function. The collect() function is used to collect the emitted values from the flow and print them to the console. When collect() is called, it suspends the coroutine and waits for values to be emitted by the Flow. The emit() function is called within the flow builder to emit each value to the collector.
Please note that emit is a suspending function provided by the Flow class in Kotlin Coroutines. It is used to emit a single value to a downstream collector.
You cannot use emit without a Flow. However, you can use it in a regular function that is not a Flow, as long as the function is marked as suspend and you have a FlowCollector object to emit values. For example:
suspend fun printNumbers() {
? ? val numbers = listOf(1, 2, 3, 4, 5)
? ? numbers.forEach {
? ? ? ? emit(it)
? ? }
}
Here, printNumbers() is a suspending function that can emit values using emit(), but it is not a Flow. You can use this function as a source for a Flow like this:
val flow = flow {
? ? printNumbers()
}
In this way, the printNumbers() function becomes a source for the Flow and the values it emits can be collected downstream.
Back to ICC with flow discussion:
Example of Flow for ICC:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun main() = runBlocking<Unit> {
val flow = flow {
for (i in 1..5) {
emit(i)
delay(1000)
}
}
launch {
flow.collect { value ->
println("Coroutine 2 received $value")
}
}
println("Coroutine 1 started")
delay(2000)
println("Coroutine 1 resuming")
flow.collect { value ->
println("Coroutine 1 received $value")
}
println("Coroutine 1 completed")
}
/*
Op =>
Coroutine 1 started
Coroutine 2 received 1
Coroutine 2 received 2
Coroutine 1 resuming
Coroutine 1 received 1
Coroutine 2 received 3
Coroutine 1 received 2
Coroutine 2 received 4
Coroutine 1 received 3
Coroutine 2 received 5
Coroutine 1 received 4
Coroutine 1 received 5
Coroutine 1 completed
*/
In the above example, we define a flow that emits the numbers 1 through 5 with a delay of 1 second between each value. We then launch two coroutines. The first coroutine collects the values emitted by the flow and prints them to the console. The second coroutine simply prints a message to indicate that it has started and resumed after a delay.
Based on the output we can see, the flow emits values to both coroutines, but they receive them in a different order. Coroutine 2 receives all values before coroutine 1, because it starts collecting before coroutine 1 resumes.
This is just a simple example, but it demonstrates the basics of using Flow to communicate between coroutines. You can use Flow to emit any kind of data stream, and you can collect the data in any way that suits your needs.
10. Blocking queues: In Kotlin, you can use java.util.concurrent Blocking Queues to communicate between coroutines. A blocking queue is a queue that blocks when you try to dequeue an element from it and the queue is empty, or if you try to enqueue an element into it and the queue is full. This means that coroutines that interact with the queue will block until they are able to perform their operation.
This is Java's BlockingQueue interface can also be used to communicate between coroutines. Blocking queues can be used to create a buffer between producers and consumers, where producers can add elements to the queue using the put() function and consumers can remove elements from the queue using the take() function. However, blocking queues can potentially block the current thread if the queue is full or empty.
import kotlinx.coroutines.*
import java.util.concurrent.ArrayBlockingQueue
fun main() = runBlocking<Unit> {
? ? val queue = ArrayBlockingQueue<String>(1)
? ? // Coroutine 1: send a message to the queue
? ? launch {
? ? ? ? val message = "Jai Shree Ram!"
? ? ? ? queue.put(message)
? ? ? ? println("Sent message: $message")
? ? }
? ? // Coroutine 2: receive the message from the queue
? ? launch {
? ? ? ? val message = queue.take()
? ? ? ? println("Received message: $message")
? ? }
? ? // Wait for all coroutines to complete
? ? joinAll()
}
/*
Op =>
Sent message: Jai Shree Ram!
Received message: Jai Shree Ram!
*/
In the above example, we create a blocking queue with a capacity of 1 element. We then launch two coroutines: the first coroutine sends a message to the Queue, and the second coroutine receives the message from the queue.
As we can see, the message is sent and received successfully using the blocking queue. The put() method blocks the first coroutine until there is room in the queue to add the message, and the take() method blocks the second coroutine until a message is available in the queue.
This is just a simple example, but it demonstrates the basics of using blocking queues to communicate between coroutines. You can use blocking queues to pass any kind of data between coroutines, and the queue will ensure that the communication is synchronized and thread-safe.
11. Callbacks: Coroutines can also communicate through callbacks, where one coroutine passes a callback function to another coroutine and the second coroutine calls the function when a certain event occurs. However, this can be less efficient than other communication methods since it requires creating additional objects and can lead to complex control flow.
In general, callbacks should be avoided when working with coroutines because callbacks can cause issues such as callback hell, error handling problems, and difficulty in maintaining control flow.
Coroutines provide their own structured concurrency, which makes it easier to write asynchronous code with a linear control flow. Instead of callbacks, you can use suspend functions, which can be called from other suspend functions or from coroutine builders like launch or async.
However, there may be situations where you need to use callbacks, such as when working with external libraries or APIs that use callbacks. In those cases, you can use the suspendCoroutine function to wrap the callback in a suspend function that can be used with coroutines.
Bonus point => BTW do you what is callback hell?
Callback hell, also known as the "pyramid of doom", is a common problem in asynchronous programming where the code becomes difficult to read and maintain due to a series of nested callbacks.
In callback hell, callbacks are nested inside other callbacks, making the code hard to understand and follow. This can make it difficult to reason about the control flow of the program, and can also make it hard to handle errors and exceptions.
getUserData(userId, function(userData) {
getUserProfile(userData.username, function(profileData) {
getFriends(profileData.userId, function(friendsData) {
getNotifications(profileData.userId, function(notificationsData) {
// Do something with all the data
});
});
});
});
In the above example, the code is using nested callbacks to fetch user data, user profile data, friends data, and notifications data. Each nested callback relies on the data returned by the previous callback, making the code difficult to read and understand.
This can lead to a number of issues, such as:
To avoid callback hell, it is recommended to use alternative approaches such as Promises, async/await, or coroutines. These approaches provide a more structured and readable way of dealing with asynchronous code.
What is Promise?
Promises are actually a JavaScript concept and not specific to Kotlin. Promises are a way of handling asynchronous operations in a more structured way. A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and allows you to attach callbacks to handle the result.
In Kotlin, we can use JavaScript Promises directly in our code if we are working with JavaScript libraries or APIs that use them. However, Kotlin also has its own Promise-like type called Deferred. Deferred is a part of the Kotlin Coroutines library and can be used to handle asynchronous operations in a similar way to Promises.
A Deferred instance represents a value that will be available in the future, and can be completed or cancelled. We can use async to create a Deferred instance and launch a coroutine that performs the asynchronous operation. We can then use await to wait for the Deferred instance to complete and retrieve its value.
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
val deferredResult = async {
// Perform some expensive computation asynchronously
delay(1000)
"Result"
}
println("Waiting for the result...")
val result = deferredResult.await()
println("Result: $result")
}
In the above example, we create a Deferred instance by calling async and launching a coroutine that performs some expensive computation asynchronously. We then use await to wait for the Deferred instance to complete and retrieve its result. This allows us to write asynchronous code in a more structured and readable way, similar to using Promises in JavaScript.
Overall, channels and actors are generally the preferred ways to communicate between coroutines in Kotlin, as they provide higher-level abstractions and are designed specifically for coroutines.
Thanks for reading till end, please comment if you have any better way of establishing the communication between coroutines and don't forget to post suggestion or questions.