Kotlin Training Program

DOWNLOAD APP

FEEDBACK

Asynchronous Programming

Introduction

Program Execution

When a program is run, following steps are followed :

  • Compilation - Program is compiled to generate binaries which will be executed by the computer. Process terminates in case of any errors.
  • Process creation - Once the binaries of a program are generated, a new Process is created by the computer in the primary memory. A program in execution is known as Process. A Process is what actually gets executed while a Program is just a set of instructions.

Thread is single sequential flow of control within a program.

Flow of control (aka Control flow) refers to the order in which instructions of a program are executed.

Thread can be thought of as a mini, lightweight process. A Process can have multiple threads hence it can perform multiple tasks in parallel.

Let’s understand the concept of Control Flow & Thread in the following sections along with example.

Synchronous Programming

The programs we have written so far perform only one task at a given point of time. For example, observe the below program :

 fun main() {
    sendVideo()
    sendImage()
    sendText()
}
 

In this program, three functions are invoked one after the other. Hence they are executed in succession i.e. sequentially and not parallelly. sendImage() starts executing only after sendVideo() has finished and sendText() starts executing only after sendImage() has finished. All three functions cannot execute in parallel. This is an example of Synchronous Programming.

Synchronous Programming is a programming technique with Single-thread model and Blocking architecture, where only one task is performed at a time.

Synchronous programming follows Single-thread model due to which there is just single control flow. Here the control flow is :

 sendVideo() -> sendImage() -> sendText()
 

This is what restricts us to perform only one task at a time. Because one task is performed at a time, other tasks are blocked, hence it is called Blocking architecture. In the above example, functions sendImage() and sendText() are blocked when sendVideo() executes. So single thread implies single control flow hence sequential execution.

To perform all three functions in parallel, we need 3 control flow executing parallelly :

 1: sendVideo()
2: sendImage()
3: sendText()
 

To have 3 control flow, we need 3 threads. A Process (program in execution) can have multiple threads. This is only possible through Asynchronous programming.

Asynchronous Programming

Applications maybe performing several tasks parallelly at the same moment. Take an example of web browser, in which you have multiple tabs open :

  • Playing music on Spotify
  • Timer running on Google
  • You preparing a presentation on Canva

All three tasks are performed parallelly by your web browser. Music being played and timer running in the background while you prepare your presentation.

For another example, consider WhatsApp. Suppose you received a video from your friend and queued it for download. While the video is being downloaded, you can type and send new messages. Moreover, you can receive more messages too. This way, WhatsApp is capable of performing multiple tasks at once.

How is this made possible? Through multiple threads i.e. Multi Threaded Programming due to which there are multiple control flows. In such parallel execution, one task being performed does not block other tasks hence it follows Non-blocking architecture.

Asynchronous Programming is a programming technique with Multi-thread model and Non-Blocking architecture, where multiple tasks can be performed at a time.

In this module, we will learn the Kotlin Coroutines API which makes Asynchronous (Async) programming much easier. Using Async Programming, you will be able to write programs that perform multiple tasks at once using a single program.

In Action

Before getting into Coroutines theory, let’s observe Coroutines in action.

Recall how we made HTTP GET requests :

 fun httpGet(url: String) = URL(url).readText()

fun main() {
		val response = httpGet("http://numbersapi.com/3/math")
    println("response = $response")
}

/* Output :

response = 3 is the third Heegner number.
 */
 

Let’s send multiple requests using the repeat statement and measure the execution time :

 fun main() {
		val timeTaken = measureTimeMillis {
        repeat(5) {
		        println("sending R#$it...")
		        getAndPrintMathFact(it)
		    }
    }
    println("\n=> Sequential execution took $sequentialTime ms")
}

fun getAndPrintMathFact(num: Int) {
    val response = httpGet("http://numbersapi.com/$num/math")
    println("MathFact($num) = $response")
}

fun httpGet(url: String) = URL(url).readText()

/* Output : 

sending R#0...
MathFact(0) = 0 is the additive identity.
sending R#1...
MathFact(1) = 1 is the multiplicative identity.
sending R#2...
MathFact(2) = 2 is a primorial, as well as its own factorial.
sending R#3...
MathFact(3) = 3 is the third Heegner number.
sending R#4...
MathFact(4) = 4 is the smallest composite number that is equal to the sum of its prime factors.

=> Sequential execution took 2210 ms

 */
 

Note :

  • This is an example of sequential execution because R#1 is sent only after response of R#0 is received. Similarly, R#2 is sent only after response of R#1 is received and so on and so forth. This is evident from the output.
  • Total time taken by these 5 requests is measured using measureTimeMillis() function. It is 2210ms which implies an average of 442ms per request because it is sequential execution.

Now let’s perform parallel execution using Coroutines and compare the execution time.

⚠️ The below program might seem overwhelming due to new words but worry not, we will understand it all in the later sections.

 fun main() {
		runBlocking {
        val parallelTime = measureTimeMillis {
            repeat(5) {
		            launch(Dispatchers.Default) {
		                println("sending R#$it")
		                getAndPrintMathFact(it)
		            }
		        }
        }
        println("\n=> Parallel execution took $parallelTime ms")
    }
}

fun getAndPrintMathFact(num: Int) {
    val response = httpGet("http://numbersapi.com/$num/math")
    println("MathFact($num) = $response")
}

fun httpGet(url: String) = URL(url).readText()

/* Output : 

sending R#0...
sending R#1...
sending R#2...
sending R#3...
MathFact(0) = 0 is the additive identity.
sending R#4...
MathFact(1) = 1 is the first figurate number of every kind, such as triangular number, pentagonal number and centered hexagonal number, to name just a few.
MathFact(2) = 2 is the smallest and the first prime number, and the only even one (for this reason it is sometimes called "the oddest prime").
MathFact(3) = 3 is the first unique prime due to the properties of its reciprocal.
MathFact(4) = 4 is the smallest composite number, its proper divisors being 1 and 2.

=> Parallel execution took 540 ms

 */
 

Note :

  • It is clear from the output that this is parallel execution. R#0 - R#3 all 4 requests are sent at nearly the same time without one waiting for the other. Then, response of R#0 is received followed by R#4 being sent. Thereafter, responses of other 4 requests R#1 - R#4 are received at nearly the same time.
  • Total execution time of parallel execution is just 540ms which is closer to the average request time of 442ms from sequential execution. This is the power of Parallel execution - by the time we we complete just one request in sequential execution, we are able to receive response of all 5 requests in parallel execution hence saving time.
  • The execution order and execution time may differ each time the program is run.

Now let’s understand the Coroutines API to get familiar with the new words used in the above program - runBlocking, coroutineScope, launch and Dispatchers.Default.

Coroutines

Introduction

Basics

Examples

Suspend function

Job

Async await

Scope Builders

Exception Handling