Kotlin async await coroutines

Asynchronous programming techniques

For decades, as developers we are confronted with a problem to solve — how to prevent our applications from blocking. Whether we’re developing desktop, mobile, or even server-side applications, we want to avoid having the user wait or what’s worse cause bottlenecks that would prevent an application from scaling.

There have been many approaches to solving this problem, including:

Before explaining what coroutines are, let’s briefly review some of the other solutions.

Threading

Threads are by far probably the most well-known approach to avoid applications from blocking.

Let’s assume in the code above that preparePost is a long-running process and consequently would block the user interface. What we can do is launch it in a separate thread. This would then allow us to avoid the UI from blocking. This is a very common technique, but has a series of drawbacks:

  • Threads aren’t cheap. Threads require context switches which are costly.
  • Threads aren’t infinite. The number of threads that can be launched is limited by the underlying operating system. In server-side applications, this could cause a major bottleneck.
  • Threads aren’t always available. Some platforms, such as JavaScript do not even support threads.
  • Threads aren’t easy. Debugging threads and avoiding race conditions are common problems we suffer in multi-threaded programming.

Callbacks

With callbacks, the idea is to pass one function as a parameter to another function, and have this one invoked once the process has completed.

This in principle feels like a much more elegant solution, but once again has several issues:

  • Difficulty of nested callbacks. Usually a function that is used as a callback, often ends up needing its own callback. This leads to a series of nested callbacks which lead to incomprehensible code. The pattern is often referred to as the titled christmas tree (braces represent branches of the tree).
  • Error handling is complicated. The nesting model makes error handling and propagation of these somewhat more complicated.
Читайте также:  Array filter php use

Callbacks are quite common in event-loop architectures such as JavaScript, but even there, generally people have moved away to using other approaches such as promises or reactive extensions.

Futures, promises, and others

The idea behind futures or promises (there are also other terms these can be referred to depending on language/platform), is that when we make a call, we’re promised that at some point it will return with an object called a Promise, which can then be operated on.

This approach requires a series of changes in how we program, in particular:

  • Different programming model. Similar to callbacks, the programming model moves away from a top-down imperative approach to a compositional model with chained calls. Traditional program structures such as loops, exception handling, etc. usually are no longer valid in this model.
  • Different APIs. Usually there’s a need to learn a completely new API such as thenCompose or thenAccept , which can also vary across platforms.
  • Specific return type. The return type moves away from the actual data that we need and instead returns a new type Promise which has to be introspected.
  • Error handling can be complicated. The propagation and chaining of errors aren’t always straightforward.

Reactive extensions

Reactive Extensions (Rx) were introduced to C# by Erik Meijer. While it was definitely used on the .NET platform it really didn’t reach mainstream adoption until Netflix ported it over to Java, naming it RxJava. From then on, numerous ports have been provided for a variety of platforms including JavaScript (RxJS).

The idea behind Rx is to move towards what’s called observable streams whereby we now think of data as streams (infinite amounts of data) and these streams can be observed. In practical terms, Rx is simply the Observer Pattern with a series of extensions which allow us to operate on the data.

In approach it’s quite similar to Futures, but one can think of a Future as returning a discrete element, whereas Rx returns a stream. However, similar to the previous, it also introduces a complete new way of thinking about our programming model, famously phrased as

«everything is a stream, and it’s observable»

This implies a different way to approach problems and quite a significant shift from what we’re used to when writing synchronous code. One benefit as opposed to Futures is that given it’s ported to so many platforms, generally we can find a consistent API experience no matter what we use, be it C#, Java, JavaScript, or any other language where Rx is available.

In addition, Rx does introduce a somewhat nicer approach to error handling.

Coroutines

Kotlin’s approach to working with asynchronous code is using coroutines, which is the idea of suspendable computations, i.e. the idea that a function can suspend its execution at some point and resume later on.

One of the benefits however of coroutines is that when it comes to the developer, writing non-blocking code is essentially the same as writing blocking code. The programming model in itself doesn’t really change.

Take for instance the following code:

This code will launch a long-running operation without blocking the main thread. The preparePost is what’s called a suspendable function , thus the keyword suspend prefixing it. What this means as stated above, is that the function will execute, pause execution and resume at some point in time.

  • The function signature remains exactly the same. The only difference is suspend being added to it. The return type however is the type we want to be returned.
  • The code is still written as if we were writing synchronous code, top-down, without the need of any special syntax, beyond the use of a function called launch which essentially kicks off the coroutine (covered in other tutorials).
  • The programming model and APIs remain the same. We can continue to use loops, exception handling, etc. and there’s no need to learn a complete set of new APIs.
  • It is platform independent. Whether we’re targeting JVM, JavaScript or any other platform, the code we write is the same. Under the covers the compiler takes care of adapting it to each platform.

Coroutines are not a new concept, let alone invented by Kotlin. They’ve been around for decades and are popular in some other programming languages such as Go. What is important to note though is that the way they’re implemented in Kotlin, most of the functionality is delegated to libraries. In fact, beyond the suspend keyword, no other keywords are added to the language. This is somewhat different from languages such as C# that have async and await as part of the syntax. With Kotlin, these are just library functions.

For more information, see the Coroutines reference.

Источник

Kotlin async await coroutines

Наряду с launch в пакете kotlinx.coroutines есть еще один построитель корутин — функция async . Эта функция применяется, когда надо получить из корутины некоторый результат.

async запускает отдельную корутину, которая выполняется параллельно с остальными корутинами. Например:

import kotlinx.coroutines.* suspend fun main() = coroutineScope < async< printHello()>println("Program has finished") > suspend fun printHello() < delay(500L) // имитация продолжительной работы println("Hello work!") >

Консольный вывод программы:

Program has finished Hello work!

Кроме того, async-корутина возвращает объект Deferred , который ожидает получения результата корутины. (Интерфейс Deferred унаследован от интерфейса Job , поэтому для также доступны весь функционал, определенный для интефейса Job)

Для получения результата из объекта Deferred применяется функция await() . Рассмотрим на примере:

import kotlinx.coroutines.* suspend fun main() = coroutineScope < val message: Deferred= async < getMessage()>println("message: $") println("Program has finished") > suspend fun getMessage() : String < delay(500L) // имитация продолжительной работы return "Hello" >

В данном случае для имитации продолжительной работы определена функция getMessage() , которая возвращает строку.

С помощью функции async запускаем корутину, которая выполняет эту функцию.

Поскольку функция getMessage() возвращает объект типа String , то возвращаемый корутиной объект представляет тип Deferred (объект Deferred типизиуется возвращаемым типом функции, то есть в данном случае типом String).

val message: Deferred = async

Далее у объекта Deferred для получения результата функции getMessage() вызываем метод await() . Он ожидает, пока не будет получен результат. Консольный вывод программы:

message: Hello Program has finished

Поскольку функция getMessage() возвращает объект типа String, то метод await() в данном случае также будет возвращать строку, которую мы могли бы, например, присвоить переменной:

val text: String = message.await()

При этом мы можем с помощью async запустить несколько корутин, которые будут выполняться параллельно:

import kotlinx.coroutines.* suspend fun main() = coroutineScope < val numDeferred1 = async< sum(1, 2)>val numDeferred2 = async < sum(3, 4)>val numDeferred3 = async < sum(5, 6)>val num1 = numDeferred1.await() val num2 = numDeferred2.await() val num3 = numDeferred3.await() println("number1: $num1 number2: $num2 number3: $num3") > suspend fun sum(a: Int, b: Int) : Int < delay(500L) // имитация продолжительной работы return a + b >

Здесь запускается три корутины, каждая из которых выполняет функцию sum() . Эта функция складывает два числа и возвращает их сумму в виде объекта Int . Поэтому корутины возвращают объект Deferred . Соответственно вызов метода await() у этого объекта возвратит объект Int , то есть сумму двух чисел. При этом все три корутины будет запущены одновременно. Например, ниже на скриншоте отладчика корутин видно, что две корутины уже работают (или находятся в состоянии Running ), и еще одна корутина только создана и ожидает запуска (состояние Created )

Coroutine async and await in Kotlin

Отложенный запуск

По умолчанию построитель корутин async создает и сразу же запускает корутину. Но как и при создании корутины с помощью launch для async-корутин можно применять технику отложенного запуска. Только в данном случае корутина запускается не только при вызове метода start объекта Deferred (который усналедован от интерфейса Job), но также и с помощью метода await() при обращении к результу корутины. Например:

import kotlinx.coroutines.* suspend fun main() = coroutineScope < // корутина создана, но не запущена val sum = async(start = CoroutineStart.LAZY)< sum(1, 2)>delay(1000L) println(«Actions after the coroutine creation») println(«sum: $») // запуск и выполнение корутины > fun sum(a: Int, b: Int) : Int

Консольный вывод программы:

Actions after the coroutine creation Coroutine has started sum: 3

Если необходимо, чтобы корутина еще до метода await() начала выполняться, то можно вызвать метод start() :

import kotlinx.coroutines.* suspend fun main() = coroutineScope < // корутина создана, но не запущена val sum = async(start = CoroutineStart.LAZY)< sum(1, 2)>delay(1000L) println(«Actions after the coroutine creation») sum.start() // запуск корутины println(«sum: $») // получаем результат > fun sum(a: Int, b: Int) : Int

Источник

Оцените статью