Coroutines: making scalable code in Kotlin

Standard

Hello, dear readers! Welcome to my blog. In this post, we will talk about Coroutines and how it helps our Kotlin applications to run with more scalability and robustness.

The Kotlin language

Created by Intellij, Kotlin is a robust language, that quickly stablished as the standard language for Android programming.

In this article we won’t learn about the language itself as the focus is learning about coroutines. If the reader wants to learn about the language itself, I recommend this good book.

Reactive programming

Another important concept to grasp is reactive programming. Reactive programming encompasses a paradigm where the parts of a program are uncoupled by using asynchronous technology, so slow operations such as IO are not blocking the execution. My article about ReactiveX talks in more detail about this concept.

Now that we have a better understand about what is Kotlin and reactive programming, let’s begin our tour in coroutines. Let’s first understand what are threads and how coroutines are not threads.

What are threads

Threads are programming units that run inside a process. A process, typically, is translated to the application itself, with memory and CPU allocated for her to consume.

Threads are like subprocesses, that a computing process allocates to do task loads. Threads have their own share of resources, like memory, for example (in Java, for example, each thread has his own object stack), so they are expensive and frequently re-used by thread pools.

An important concept to keep it in mind is that threads are not necessarily parallel: when a process allocates threads, they are been executed by CPU cores.

If we have just one core, for example, and create two threads, they won’t really run in parallel. What will happen is that the core will switch between threads doing a little of execution in each one, but a lot of times extremely fast, making it look like is really running at the same time. This becomes apparent when one of the threads has to run an IO-blocking operation: this makes the whole processing to freeze, including the work on the other thread.

We only achieve parallelism when running inside an application with multiple cores: in this case, each thread can be run inside one core, and indeed, in this case, the CPU cores are really running in parallel.

Normally we don’t need to make any low-level code to make threads run on different cores, but it is an important thing to know, because if for example, we create a lot more threads then cores are available, we could end up with an application that is slower then a single-threaded, sequential one.

That’s because all execution control to switch executions and overburden to create a massive amount of threads will end up demanding more from the hardware than simply running everything in one thread. So, it is important to remember that, like everything in life, threads are not a silver bullet for everything.

So now we know what threads are. What about coroutines?

What are coroutines

Coroutines are a light-weighted programming unit, available by Kotlin. What that means is that coroutines are not threads, but actually functions (like sets of instructions) that run inside thread pools.

One thread runs multiple coroutines, and a coroutine can start processing in one thread, be suspended (more later), and resumed in another thread.

This is achieved by Kotlin’s inner coroutine support, where dispatchers are responsible for controlling how the coroutine will be executed if it will running everything in just one thread, a thread pool, or unconfined.

So, as we can see, coroutines are kind like threads, but more light and without the burden of resource consumption threads have. This also means that we can allocate a lot more coroutines then we would typically do with threads since the dispatcher will control the number of threads used.

Coroutine suspension

We talked briefly about coroutine suspension. What did we mean by that?

When running our code, sometimes our task will need to wait for operations, such as I/O, to be performed before continuing. When a coroutine reaches a point where it has to wait for something to runs, it enters a suspension state. In this state, the code will wait for the operation to complete before continuing to execute the coroutine.

One very important detail to keep in mind is that this state is not blocking: once the coroutine stops to wait for the operation, her thread is freed to continue running another thread meanwhile the operation is done.

Well, but let’s stop with all theory and get to the point we are all waiting for: begin the coding!

Our first coroutine

Let’s begin with a simple example. All code from the lab can be found here or at the end of the article. I strongly recommend using Intellij IDEA as IDE as it will make it really simple to run the examples (just click in the application file and run it), importing as a Gradle project. The project runs with Kotlin 1.3 and JDK 8.

In order to use coroutines, we need to add a library to build.gradle, to add his support:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"

    compile "io.github.microutils:kotlin-logging:1.8.3"
    compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5"
    compile "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.3.5"
    compile "org.slf4j:slf4j-simple:1.7.29"
}

Let’s suppose we have a task that it takes some time to complete – we will simulate this by using a delay – and this task is necessary to run a second task. Let’s implement a simple code, with a coroutine:

package com.alexandreesl

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import mu.KotlinLogging

object Application {

    private val logger = KotlinLogging.logger {}

    @JvmStatic
    fun main(args: Array<String>) {


        val deferred = CoroutineScope(IO).async {
            logger.info { "running on ${Thread.currentThread().name}" }
            delay(2000)
            123
        }

        logger.info { "running on ${Thread.currentThread().name}" }

        val resp = deferred.getCompleted()

        logger.info { "the result is $resp" }


    }

}

We have a first glance on some concepts, such as coroutine scopes and dispatchers – the IO passed to the scope. We will talk more about scopes later, but lets, for now, understand what are dispatchers.

Dispatchers, as talked before, are responsible for starting and controlling coroutine executions. The most commonly used ones are Main (used in Android development, it makes the code run inside the UI thread, a requirement from the Android platform to certain operations), and IO.

The IO dispatcher uses a thread-poll with the same size as the machine’s cores, making them ideal for parallel tasks. There are other ones such as previously mentioned Unconfined, which keeps switching from thread to thread as new scopes with other dispatchers are created, but commonly Main and IO are the ones we will use most of the time.

The code inside the braces is the coroutine itself. The receiver method, in this case async, is called a coroutine builder, as it instantiates the coroutine and supplies it to the dispatcher. There are other builders, such as launch, designed for fire-and-forget operations.

Async is used when we want to run something that returns a result, as it supplies a Deferred. Think of a deferred to be like a promise or future: It allows us an interface for receiving a result from asynchronous processing.

When we run the code, this happens:


Exception in thread "main" java.lang.IllegalStateException: This job has not completed yet
	at kotlinx.coroutines.JobSupport.getCompletedInternal$kotlinx_coroutines_core(JobSupport.kt:1198)
	at kotlinx.coroutines.DeferredCoroutine.getCompleted(Builders.common.kt:98)
	at com.alexandreesl.Application.main(Application.kt:26)

Process finished with exit code 1

What happened?! The problem is, we ran our coroutine in another thread, but our main code is running inside the main thread, which keeps running and tried to get the result from the deferred before is completed – getCompleted() is also an experimental method marked for removal, so it is not recommended. Let’s refactor our code to a better way of running our coroutine:

package com.alexandreesl

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import mu.KotlinLogging

object Application {

    private val logger = KotlinLogging.logger {}

    @JvmStatic
    fun main(args: Array<String>) {


        val deferred = CoroutineScope(IO).async {
            logger.info { "running on ${Thread.currentThread().name}" }
            delay(2000)
            123
        }

        runBlocking {

            logger.info { "running on ${Thread.currentThread().name}" }

            val resp = deferred.await()

            logger.info { "the result is $resp" }

        }



    }

}

Notice we added a runBlocking code block. This special coroutine builder has the purpose of creating a coroutine which will block the current thread (making it possible to run simple sample codes and test cases such as these). It is important to keep in mind that in real production code we need to avoid using this as much as possible, since we are making blocking code this way.

We also changed to use await to get the result of the coroutine. This method is at the heart of coroutines usage: is by calling this that we make our coroutine to wait for the completion of the task before continuing. As talked before, this operation is not blocking.

When we run the code, we receive something like this in the console:

[DefaultDispatcher-worker-1] INFO com.alexandreesl.Application - running on DefaultDispatcher-worker-1
[main] INFO com.alexandreesl.Application - running on main
[main] INFO com.alexandreesl.Application - the result is 123

Process finished with exit code 0

As we can see, we have logs in different threads, showing our coroutine indeed ran on another thread.

Now, there is just one thing missing from the explanation: what is that CoroutineScope thing? Let’s talk about this now.

Coroutine scopes

When passing our coroutine builders, each one of them creates a coroutine. Let’s take this code snippet as an example:

runBlocking {

            val deferred = CoroutineScope(IO).async {
                logger.info { "I am the first Coroutine, I am running on ${Thread.currentThread().name}" }
                delay(2000)

                val job = CoroutineScope(IO).launch {
                    logger.info { "I am the second Coroutine, I am running on ${Thread.currentThread().name}" }
                    delay(2000)
                }

                job.join()
                123

            }

            logger.info { "running on ${Thread.currentThread().name}" }

            val resp = deferred.await()

            logger.info { "the result is $resp" }

        }

When running, we will get the following from the console:

[main] INFO com.alexandreesl.Application - running on main
[DefaultDispatcher-worker-1] INFO com.alexandreesl.Application - I am the first Coroutine, I am running on DefaultDispatcher-worker-1
[DefaultDispatcher-worker-3] INFO com.alexandreesl.Application - I am the second Coroutine, I am running on DefaultDispatcher-worker-3
[main] INFO com.alexandreesl.Application - the result is 123

As we can see it, each coroutine runs on his own thread, showing that indeed each builder created a coroutine.

Let’s suppose that we have some IO code that, due to some reason, we have scenarios where we will have to cancel the execution – for example, if a database session hangs and the code is forever hang in an non-terminal state – , can we do this with coroutines? Let’s find it out.

We change our code like this and run again:

package com.alexandreesl

import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import mu.KotlinLogging

object Application {

    private val logger = KotlinLogging.logger {}

    @JvmStatic
    fun main(args: Array<String>) {

        runBlocking {

            val deferred = CoroutineScope(IO).async {
                logger.info { "I am the first Coroutine, I am running on ${Thread.currentThread().name}" }
                delay(2000)

                val job = CoroutineScope(IO).launch {
                    logger.info { "I am the second Coroutine, I am running on ${Thread.currentThread().name}" }
                    delay(2000)
                }

                job.join()
                123

            }

            logger.info { "running on ${Thread.currentThread().name}" }

            deferred.cancelAndJoin()
            

        }

    }

}

After running, we see something like this on the terminal:

[main] INFO com.alexandreesl.Application - running on main
[DefaultDispatcher-worker-1] INFO com.alexandreesl.Application - I am the first Coroutine, I am running on DefaultDispatcher-worker-1

Only the first coroutine got the chance to start, and immediately after we got to the cancellation. But what would happen if we didn’t declared the two in the same scope?

We change the code and run again:

       runBlocking {

            val deferred = CoroutineScope(IO).async {
                logger.info { "I am the first Coroutine, I am running on ${Thread.currentThread().name}" }
                delay(2000)
                123
            }

            val job = CoroutineScope(IO).launch {
                logger.info { "I am the second Coroutine, I am running on ${Thread.currentThread().name}" }
                delay(2000)
            }

            logger.info { "running on ${Thread.currentThread().name}" }

            deferred.cancelAndJoin()
            
        }

This leads to:

[DefaultDispatcher-worker-1] INFO com.alexandreesl.Application - I am the first Coroutine, I am running on DefaultDispatcher-worker-1
[DefaultDispatcher-worker-3] INFO com.alexandreesl.Application - I am the second Coroutine, I am running on DefaultDispatcher-worker-3
[main] INFO com.alexandreesl.Application - running on main

Wait, the second one got to run? That’s because she ran outside the scope. In order to make the other coroutine to cancel as well, we would need to manually cancel her too.

That’s the main goal behind coroutine scopes: it allows us to group coroutines together, so we can make operations such as cancellations in a grouped manner, instead of making all the work one by one.

Suspending functions

Now let’s organize our code, moving our first example to another class. We create the following class:

package com.alexandreesl.examples

import kotlinx.coroutines.*
import mu.KotlinLogging

class BasicExample {

    private val logger = KotlinLogging.logger {}

    suspend fun runExample() {
        

            val deferred = CoroutineScope(Dispatchers.IO).async {
                logger.info { "I am the first Coroutine, I am running on ${Thread.currentThread().name}" }
                delay(2000)

                val job = CoroutineScope(Dispatchers.IO).launch {
                    logger.info { "I am the second Coroutine, I am running on ${Thread.currentThread().name}" }
                    delay(2000)
                }

                job.join()
                123

            }

            logger.info { "running on ${Thread.currentThread().name}" }

            val resp = deferred.await()

            logger.info { "the result is $resp" }

        
    }

}

We created a class that has a suspending function. Suspending functions are functions that, in her nature, their whole body is a coroutine. That’s why we can declare operations such as await without the need of a runBlocking code block, for example.

Now we change main to this:

package com.alexandreesl

import com.alexandreesl.examples.BasicExample
import kotlinx.coroutines.*
import kotlinx.coroutines.Dispatchers.IO
import mu.KotlinLogging

object Application {

    private val logger = KotlinLogging.logger {}

    @JvmStatic
    fun main(args: Array<String>) {

        runBlocking {

          val basicExample =  BasicExample()

            basicExample.runExample()

        }

    }

}

And after running again, we see everything is running as before. In a nutshell, suspending functions are a neat way of running most of our code inside coroutines.

Coroutine contexts

Another concept to grasp is coroutine contexts. Basically, a context is the composition of two things: the scope and the dispatcher.

Kotlin’s coroutines have another neat feature that is a builder called withContext. Using this feature, we can switch between contexts at will. Let’s see a example:

package com.alexandreesl.examples

import kotlinx.coroutines.delay
import kotlinx.coroutines.newSingleThreadContext
import kotlinx.coroutines.withContext
import mu.KotlinLogging

class ContextExample {

    private val logger = KotlinLogging.logger {}

    suspend fun runExample() {

        val context1 = newSingleThreadContext("MyOwnThread1")
        val context2 = newSingleThreadContext("MyOwnThread2")

        withContext(context1) {

            logger.info { "I am a coroutine, I am running on ${Thread.currentThread().name}" }

            withContext(context2) {

                logger.info { "I am also a coroutine, I am running on ${Thread.currentThread().name}" }
                delay(3000)

            }

            logger.info { "I am the previous coroutine, I am running on ${Thread.currentThread().name}" }

            delay(3000)

        }

    }

}

Here we created two single-thread contexts and switched between then inside two nested coroutines. If we run and look at the console, we will see something like this:

[MyOwnThread1] INFO com.alexandreesl.examples.ContextExample - I am a coroutine, I am running on MyOwnThread1
[MyOwnThread2] INFO com.alexandreesl.examples.ContextExample - I am also a coroutine, I am running on MyOwnThread2
[MyOwnThread1] INFO com.alexandreesl.examples.ContextExample - I am the previous coroutine, I am running on MyOwnThread1

Showing that we successfully ran our coroutines with different contexts.

Processing streams with coroutines

In all our previous examples, we just returned a single value. What if we need to emit multiple values, that will be processed as they are generated?

For this purpose, Kotlin coroutines have flows. With flows, we can make data streams to emit data at the rate it is produced. Let’s see a example.

Let’s suppose we have a sequence of 10 numbers, that have IO operations between then and only after the operations the numbers can be emitted. The following code simulates this:

package com.alexandreesl.examples

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import mu.KotlinLogging
import java.time.LocalDateTime

class FlowExample {

    private val logger = KotlinLogging.logger {}

    suspend fun runExample() {

        flow {
            for (number in 1..10) {
                logger.info { "I am processing the number $number at ${LocalDateTime.now()}" }
                delay(2000)
                emit(number)
            }
        }.collect { item ->
            logger.info { "Receiving number $item at ${LocalDateTime.now()}" }
        }

    }

}

When running, we will get logs like this:

[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 1 at 2020-09-24T19:55:35.579
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 1 at 2020-09-24T19:55:37.592
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 2 at 2020-09-24T19:55:37.592
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 2 at 2020-09-24T19:55:39.597
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 3 at 2020-09-24T19:55:39.597
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 3 at 2020-09-24T19:55:41.600
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 4 at 2020-09-24T19:55:41.600
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 4 at 2020-09-24T19:55:43.605
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 5 at 2020-09-24T19:55:43.606
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 5 at 2020-09-24T19:55:45.608
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 6 at 2020-09-24T19:55:45.608
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 6 at 2020-09-24T19:55:47.611
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 7 at 2020-09-24T19:55:47.611
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 7 at 2020-09-24T19:55:49.616
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 8 at 2020-09-24T19:55:49.616
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 8 at 2020-09-24T19:55:51.621
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 9 at 2020-09-24T19:55:51.622
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 9 at 2020-09-24T19:55:53.627
[main] INFO com.alexandreesl.examples.FlowExample - I am processing the number 10 at 2020-09-24T19:55:53.627
[main] INFO com.alexandreesl.examples.FlowExample - Receiving number 10 at 2020-09-24T19:55:55.628

Showing how the data is been processed as it is generated. Flows are like other collections we have in Kotlin: We can use operations such as map, reduce, filter etc. Let’s improve our example to demonstrate this.

We change our code to filter only even numbers, log each filtered number and sum it all numbers from the flow:

package com.alexandreesl.examples

import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.reduce
import mu.KotlinLogging
import java.time.LocalDateTime

class FlowExample {

    private val logger = KotlinLogging.logger {}

    suspend fun runExample() {

        val result = flow {
            for (number in 1..10) {
                logger.info { "I am processing the number $number at ${LocalDateTime.now()}" }
                delay(2000)
                emit(number)
            }
        }.filter { it % 2 == 0 }
            .map {
                logger.info { "processing number $it" }
                it
            }
            .reduce { accumulator, value ->
                accumulator + value
            }
        logger.info { "result: $result" }

    }

}

Notice that we didn’t need to add a await for the operation: since reduce is a terminal operation, Kotlin implicitly suspends the execution for us to receive the result.

We can also transform simple collections to execute as flows, using the asFlow() function, like this:

(1..5).asFlow().collect {
            logger.info { "item: $it" }
        }

This will produce:

[main] INFO com.alexandreesl.examples.FlowExample - item: 1
[main] INFO com.alexandreesl.examples.FlowExample - item: 2
[main] INFO com.alexandreesl.examples.FlowExample - item: 3
[main] INFO com.alexandreesl.examples.FlowExample - item: 4
[main] INFO com.alexandreesl.examples.FlowExample - item: 5

One last thing for us to learn before wrapping it up on Flows is the flowOn() function. With this function, we can define the coroutine context in which the flow will run. For example, if we want our flow to run in a specific thread, we can do this:

       val context1 = newSingleThreadContext("MyOwnThread1")

        val result = flow {
            for (number in 1..10) {
                logger.info { "I am processing the number $number at ${LocalDateTime.now()}" }
                delay(2000)
                emit(number)
            }
        }
            .flowOn(context1)
            .filter { it % 2 == 0 }
            .map {
                logger.info { "processing number $it" }
                it
            }
            .reduce { accumulator, value ->
                accumulator + value
            }
        logger.info { "result: $result" }

This produces the following output:

[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 1 at 2020-09-24T20:22:36.840
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 2 at 2020-09-24T20:22:38.854
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 3 at 2020-09-24T20:22:40.855
[main] INFO com.alexandreesl.examples.FlowExample - processing number 2
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 4 at 2020-09-24T20:22:42.860
[main] INFO com.alexandreesl.examples.FlowExample - processing number 4
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 5 at 2020-09-24T20:22:44.866
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 6 at 2020-09-24T20:22:46.871
[main] INFO com.alexandreesl.examples.FlowExample - processing number 6
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 7 at 2020-09-24T20:22:48.877
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 8 at 2020-09-24T20:22:50.882
[main] INFO com.alexandreesl.examples.FlowExample - processing number 8
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 9 at 2020-09-24T20:22:52.887
[MyOwnThread1] INFO com.alexandreesl.examples.FlowExample - I am processing the number 10 at 2020-09-24T20:22:54.893
[main] INFO com.alexandreesl.examples.FlowExample - processing number 10
[main] INFO com.alexandreesl.examples.FlowExample - result: 30

Proving our configuration was a success – notice that some logs were on main thread, since only the flow code is running in other context.

Communication between coroutines

Sometimes, there are scenarios where we need to make two coroutines to work with each other, like when we have to send data to a API only after some heavy IO processing is done with that same data. Each one of this operations must be done in different coroutines, to allow parallel execution.

One way of doing this is by chaining the coroutines together, and another one is using channels.

Channels, as the name implies, allows coroutines to share data between them. Let’s see an example.

Suppose we have two coroutines, where one of then will delay by some seconds, to represent the IO processing, and after that will send the data to a channel to be processed by another coroutine:

package com.alexandreesl.examples

import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import mu.KotlinLogging

class ChannelExample {

    private val logger = KotlinLogging.logger {}

    suspend fun runExample() {

        val channel = Channel<Int>()

        CoroutineScope(Dispatchers.IO).launch {
            for (item in 1..10) {
                // delay to represent IO
                delay(2000)
                logger.info { "Sending: $item" }
                channel.send(item)
            }
            // closing the channel, we are done
            channel.close()
        }

        val deferred = CoroutineScope(Dispatchers.IO).async {
            try {
                while (true) {
                    val received = channel.receive()
                    logger.info { "Received: $received" }
                }
            } catch (e: ClosedReceiveChannelException) {
                // this happens when the channel is closed
            }
            //signalling the process has ended
            true
        }

        deferred.await()

    }


}

This will produce the following:

[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Sending: 1
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Received: 1
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Sending: 2
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Received: 2
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Sending: 3
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Received: 3
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Sending: 4
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Received: 4
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Sending: 5
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Received: 5
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Sending: 6
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Received: 6
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Sending: 7
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Received: 7
[DefaultDispatcher-worker-3] INFO com.alexandreesl.examples.ChannelExample - Sending: 8
[DefaultDispatcher-worker-2] INFO com.alexandreesl.examples.ChannelExample - Received: 8
[DefaultDispatcher-worker-2] INFO com.alexandreesl.examples.ChannelExample - Sending: 9
[DefaultDispatcher-worker-2] INFO com.alexandreesl.examples.ChannelExample - Received: 9
[DefaultDispatcher-worker-2] INFO com.alexandreesl.examples.ChannelExample - Sending: 10
[DefaultDispatcher-worker-2] INFO com.alexandreesl.examples.ChannelExample - Received: 10

Notice that sometimes both receiving and sending are done in the same thread. This is a good example to show how Kotlin keeps re-using the same threads to run different coroutines, in order to optimize the execution

As we can see, the channel ran successfully, sending data from one coroutine to another.

Conclusion

And so we concluded our tour about coroutines. With a simple interface, but powerful features, coroutines are a excellent tool for developing robust solutions in Kotlin. All code from our lab can be found here.

Thank you for following me on my blog, until next time.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.