
The Art of Not Waiting
Kotlin · Coroutines
Suppose you are building a tourism dashboard that shows weather and traffic information for a particular city. Ideally, each component updates independently. Weather and traffic data are retrieved from separate external services, then converted into user-friendly reports by a report service. Because the weather and traffic services operate independently, their requests should run in parallel. The report service, however, can create a report only once the required data has arrived. Kotlin makes this orchestration elegant with just a few lines of code. In this post, I'll walk you through how Kotlin Coroutines make it possible.
The external services
For brevity, in this example we will not take presentation logic into account, but only concentrate on concurrent retrieval and formatting of data from different sources. The services to retrieve the weather and traffic information return responses as more or less raw data. We want to create user-friendly reports of the raw data using the ReportService. A report can only be created after the service that supplies the data has returned a response. Here are the interfaces for the traffic and weather services.
interface TrafficService {
suspend fun getTrafficInfo(cityName: String): TrafficData
}
interface WeatherService {
suspend fun getWeatherInfo(cityName: String): WeatherData
}The report service looks like this:
interface ReportService {
suspend fun createTrafficReport(trafficData: TrafficData): String
suspend fun createWeatherReport(weatherData: WeatherData): String
}In a real-life situation, the report service would no doubt be designed in a more object-oriented way, using polymorphism and inheritance, but we kept it simple to keep the focus on the coroutine implementation. A method qualified with suspend runs within a Kotlin coroutine context and can be suspended and resumed by the Kotlin runtime. That way, when the thread in which the suspendable method runs is idle, for instance waiting for the underlying service to respond, Kotlin can in the meantime do other work that is not dependent on this call. Note that a suspend method does not, by itself, run in a thread or in parallel with other code. It only provides the means for client code to do so if needed.
Calling the services efficiently using coroutines
We will explain the concepts that we are going to use in a step-by-step way. We start out with old-fashioned serial code and then convert it to code that lets tasks run in parallel, and block waiting for the next step until all data is available. Consider the following simple code. The data for both the weather and traffic for Rijswijk are retrieved. Both are then formatted to reports for the dashboard. For simplicity, we will imagine that the service methods are not qualified with suspend for this code snippet.
package com.mungra
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
class RunSerially {
fun runSerially() {
val weatherService: WeatherService
val trafficService: TrafficService
val reportService: ReportService
val weatherData = weatherService.getWeatherInfo("Rijswijk")
val trafficData = trafficService.getTrafficInfo("Rijswijk")
val weatherReport = reportService.createWeatherReport(weatherData)
val trafficReport = reportService.createTrafficReport(trafficData)
}
}Every step is executed serially. The traffic service is consulted only after the weather data has been received. However, traffic and weather are retrieved from different independent services and could be retrieved in parallel, thus avoiding one service having to wait for the other. In contrast, the report service methods need the weather or traffic data to create the reports. The weather report can only be created after the weatherData has been retrieved for the city in question. The same goes for the traffic data: the report service method needs the trafficData. How will we make the calls to the report service wait for the weatherService or trafficService threads to finish? We will solve all of these problems with minimal code.
runBlocking
By default, Kotlin does not run in a coroutine context. To run code in a coroutine context, we put it here in a runBlocking code block. All code within a runBlocking block still runs sequentially by default, but within the block it is now possible to take advantage of coroutine functionality.
runBlocking {
// code that executes within a coroutine context
}async and await
Since the methods of the weather and traffic services are suspendable methods, we can tell Kotlin to spawn a thread for each service call and have them execute in parallel. The way we will do this is to call each service from within an 'async' code block.
val weatherDataDeferred = async {
weatherService.getWeatherInfo("Rijswijk")
}
val weatherData = weatherDataDeferred.await()
// the call to the weather service runs in a separate thread, but this
// line still waits for it, because .await() suspends until the
// result comes backThe code displayed above will perform the following functions:
- ● Kotlin spawns a separate thread for the code within the async and performs it.
- ● The result of the async is actually a variable of type Deferred<T>, in this case Deferred<WeatherData>.
- ● The call to .await() suspends this line until the thread finishes, so the code after it does not run until the weather data is ready.
- ● The expression that the code in the thread eventually evaluates to, in this case the result of the service call, will become available if and only if the service has responded.
- ● The thread itself finishes and its data will be garbage-collected.
We will have 2 async blocks, for the traffic and weather services.
package com.mungra
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() {
val weatherService: WeatherService
val trafficService: TrafficService
val reportService: ReportService
runBlocking {
// Start both calls immediately -- neither is awaited yet,
// so they run concurrently.
val weatherDataDeferred = async {
weatherService.getWeatherInfo("Rijswijk")
}
val trafficDataDeferred = async {
trafficService.getTrafficInfo("Rijswijk")
}
// Now suspend until both results are in.
val weatherData = weatherDataDeferred.await()
val trafficData = trafficDataDeferred.await()
launch {
val weatherReport = reportService.createWeatherReport(weatherData)
} // runs as soon as weatherData is available
launch {
val trafficReport = reportService.createTrafficReport(trafficData)
} // runs as soon as trafficData is available
}
}The weather report service is called with weatherData as a parameter. Because we already awaited weatherDeferred just before, weatherData is guaranteed to be available at this point, so createWeatherReport() can start straight away. The same goes for trafficData. Both reports are created only once the data they depend on has actually arrived, but neither has to wait around any longer than that.
What do the launch blocks do?
When code is in a launch block, it executes in a separate thread, just like with async. Async, however, is used for code blocks that eventually return a value, and launch is used when no value is returned. In this case, we use launch to create the reports in parallel. Remember that the report service methods are also suspendable methods. The traffic report is not dependent on the weather data and vice versa.
About the Author
Dinesh is a Senior Java Developer at Qualogy. He has a Master of Science in Computer Science from the Vrije Universiteit Amsterdam and has almost 30 years of experience at various Dutch clients in government and banking, among others, primarily as a Java developer, and more recently in Kotlin. He lives in Almere, The Netherlands, with his wife and daughter, where he tends to an ever-expanding collection of comic books.