Async await kotlin android

Корутины Kotlin: как работать асинхронно в Android

May 6, 2020 · 6 min read

Kotlin предоставляет корутины, которые помогают писать асинхронный код синхронно. Android — это однопоточная платформа, и по умолчанию все работает на основном потоке (потоке UI). Когда настает время запускать операции, несвязанные с UI (например, сетевой вызов, работа с БД, I/O операции или прием задачи в любой момент), мы распределяем задачи по различным потокам и, если нужно, передаем результат обратно в поток UI.

Android имеет свои механизмы для выполн е ния задач в другом потоке, такие как AsyncTask, Handler, Services и т.д. Эти механизмы включают обратные вызовы, методы post и другие приемы для передачи результата между потоками, но было бы лучше, если бы можно было писать асинхронный код так же, как синхронный.

С корутиной код выглядит легче. Нам не нужно использовать обратный вызов, и следующая строка будет выполнена, как только придет ответ. Можно подумать, что вызов функции из основного потока заблокирует её к тому времени, как ответ вернется, но с корутиной все иначе. Она не будет блокировать поток Main или любой другой, но все еще может выполнять код синхронно. Подробнее.

Сравним корутины с потоком

В примере выше допустим, что мы создаем новый поток, и после завершения задачи передаем результат в поток UI. Для того, чтобы сделать это правильно, нужно понять и решить несколько проблем. Вот список некоторых:

1. Передача данных из одного потока в другой — это головная боль. Так еще и грязная. Нам постоянно нужно использовать обратные вызовы или какой-нибудь механизм уведомления.

2. Потоки стоят дорого. Их создание и остановка обходится дорого, включает в себя создание собственного стека. Потоки управляются ОС. Планировщик потоков добавляет дополнительную нагрузку.

3. Потоки блокируются. Если вы выполняете такую простую задачу, как задержка выполнения на секунду (Sleep), поток будет заблокирован и не может быть использован для другой операции.

4. Потоки не знают о жизненном цикле. Они не знают о компонентах Lifecycle (Activity, Fragment, ViewModel). Поток будет работать, даже если компонент UI будет уничтожен, что требует от нас разобраться с очисткой и утечкой памяти.

Как будет выглядеть ваш код с большим количеством потоков, Async и т.д.? Мы можем столкнуться с большим количеством обратных вызовов, методов обработки жизненного цикла, передач данных из одного места в другое, что затруднит их чтение. В целом, мы потратили бы больше времени на устранение проблем, а не на логику программы.

Отметим , что это не просто другой способ асинхронного программирования , это другая парадигма.

Корутины легкие и супербыстрые

Пусть код скажет за себя.

Я создам 10к потоков, что вообще нереалистично, но для понимания эффекта корутин пример очень наглядный:

Здесь каждый поток ожидает 1 мс. Запуск этой функции занял около 12,6 секунд. Теперь давайте создадим 100к корутин (в 10 раз больше) и увеличим задержку до 10 секунд (в 10000 раз больше). Не волнуйтесь про “runBlocking” или “launch” (конструкторах Coroutine).

14 секунд. Сама задержка составляет 10 секунд. Это очень быстро. Создание 100 тысяч потоков может занять целую вечность.

Если вы посмотрите на метод creating_10k_Thread(), то увидите, что существует задержка в 1 мс. Во время нее он заблокирован, т.е. ничего не может делать. Вы можете создать только определенное количество потоков в зависимости от количества ядер. Допустим, возможно создать до 8 потоков в системе. В примере мы запускаем цикл на 10000 раз. Первые 8 раз будут созданы 8 потоков, который будут работать параллельно. На 9-й итерации следующий поток не сможет быть создан, пока не будет доступного. На 1 мс поток блокируется. Затем создастся новый поток и по итогу блокировка на 1мс создает задержку. Общее время блокировки для метода составит 10000/ мс. А также будет использоваться планировщик потоков, что добавит дополнительной нагрузки.

Для creatingCoroutines() мы установили задержку в 10 сек. Корутина приостанавливается, не блокируется. Пока метод ждет 10 секунд до завершения, он может взять задачу и выполнить ее после задержки. Корутины управляются пользователем, а не ОС, что делает их быстрее. В цифрах, каждый поток имеет свой собственный стек, обычно размером 1 Мбайт. 64 Кбайт — это наименьший объем пространства стека, разрешенный для каждого потока в JVM, в то время как простая корутина в Kotlin занимает всего несколько десятков байт heap памяти.

Читайте также:  Самый лучший мр3 плеер для андроида

Еще пример для лучшего понимания:

Во фрагменте 1 мы последовательно вызываем методы fun1 и fun2 в основном потоке. На 1 секунду поток будет заблокирован. Теперь рассмотрим пример с корутиной.

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

Корутина может обеспечить высокий уровень параллелизма с небольшими нагрузками. Несколько потоков также могут обеспечить параллелизм, но у них есть блокировка и переключение контекста. Корутина не блокирует, а приостанавливает поток для других задач. Большое количество корутин, выполняющих маленькие задачи, эффективнее, чем планировщик, поэтому тысячи корутин работают быстрее, чем десятки потоков.

Как же корутина приостанавливает свою работу?

Если вы посмотрите на выход, то увидите, что ‘completionHandler’ выполняется после завершения ‘asyncOperation’. ‘asyncOperation’ выполняется в фоновом потоке, а ‘completionHandler’ ожидает его завершения. В ‘completionHandler’ происходит обновление textview. Давайте рассмотрим байтовый код метода ‘asyncOperation’.

Во второй строке есть новый параметр под названием ‘continuation’, добавленный к методу asyncOperation. Continuation (продолжение) — это рабочий вариант для приостановки кода. Продолжение добавляется в качестве параметра к функции, если она имеет модификатор ‘suspend’. Также он сохраняет текущее состояние программы. Думайте о нем как о передаче остальной части кода (в данном случае метода completionHandler()) внутрь оболочки Continuation. После завершения текущей задачи выполнится блок продолжения. Поэтому каждый раз, когда вы создаете функцию suspend, вы добавляете в нее параметр продолжения, который обертывает остальную часть кода из той же корутины.

Coroutine очень хорошо работает с Livedata, Room, Retrofit и т.д. Еще один пример с корутиной:

Источник

Погружение в Async-Await в Android

В предыдущей статье я сделал беглый обзор async-await в Android. Теперь пришло время погрузиться немного глубже в грядущий функционал kotlin версии 1.1.

Для чего вообще async-await?

Когда мы сталкиваемся с длительными операциями, такими как сетевые запросы или транзакции в базу данных, то надо быть уверенным, что запуск происходит в фоновом потоке. Если же забыть об этом, то можно получить блокировку UI потока еще до того, как задача закончится. А во время блокировки UI пользователь не сможет взаимодействовать с приложением.

К сожалению, когда мы запускаем задачу в фоне, то не можем использовать результат тут же. Для этого нам потребуется некая разновидность callback’а. Когда callback будет вызван с результатом, только тогда мы сможем продолжить, например запустить еще один сетевой запрос.

Простой пример того, как люди приходят к «callback hell«: несколько вложенных callback’ов, все ждут вызова когда длогоиграющая операция закончится.

Этот кусок кода представляет три сетевых запроса, где в конце отправляется сообщение в главный поток, чтобы обновить некий TextView.

Исправляем с помощью async-await

С помощью async-await можно привести этот код к более императивному стилю с той же функциональностью. Вместо отправки callback’а можно вызвать «замораживающий» метод await, который позволит использовать результат так же, словно он был вычислен в синхронном коде:

Этот код все еще делает три сетевых запроса и обновляет TextView в главном потоке, и не блокирует UI!

Погоди… Что?

Если мы будет использовать библиотеку AsyncAwait-Android, то получим несколько методов, два из которых async и await.

Метод async позволяет использовать await и изменяет способ получения результата. При входе в метод, каждая строка будет выполнена синхронно пока не достигнет точки «заморозки»(вызова метода await). По факту, это все, что делает async — позволяет не перемещать код в фоновый поток.

Метод await позволяет делать вещи асинхронно. Он принимает «awaitable» в качестве параметра, где «awaitable» — какая-то асинхронная операция. Когда вызывается await, он регистрируется в «awaitable», чтобы получить уведомление, когда операция закончится, и вернуть результат в метод asyncUI. Когда «awaitable» завершится, он выполнит оставшуюся часть метода, при этом передав туда результат.

Магия

Все это похоже на магию, но тут нет никакого волшебства. На самом деле компилятор котлина трансформирует coroutine (то, что находится в рамках async) в стейт-машину(конечный автомат). Каждое состояние которого — это часть кода из coroutine, где точка «заморозки»(вызов await) означает конец состояния. Когда код, переданный в await, завершается, выполнение переходит к следующему состоянию, и так далее.

Рассмотрим простую версию кода, представленного ранее. Мы можем посмотреть, какие создаются состояния, для этого отметим каждый вызов await:

Читайте также:  Создать ярлык android studio

Эта coroutin’a имеет три состояния:

  • Начальное состояние, до вызова await
  • После первого вызова await
  • После воторого вызова await

Этот код будет скомпилирован в такую стейт-машин(псевдо-байт-код):

После захода в стейт-машину будут выполнены label ==0 и первый блок кода. Когда будет достигнут await, label обновится, и стейт-машина перейдет к выполнению кода, переданного в await. После этого выполнение продолжится с точки resume.

После завершения задачи, отправленной в await, будет вызван метод стейт-машины resume(data) для выполнения следующй части. И так будет продолжаться, пока не будет достигнуто последнее состояние.

Обработка исключений

В случае завершения «awaitable» с ошибкой, стейт-машина получит уведомление об этом. На самом деле метод resume принимает дополнительный Throwable параметр, и, когда выполняется новое состояние, этот параметр проверяется на равенство null. Если параметр null, то Throwable пробрасывается наружу.

Поэтому можно использовать оператор try/catch как обычно:

Многопоточность

Метод await не гарантирует запуск awaitable в фоновом потоке, а просто регистрирует слушателя, которые реагирует на завершение awaitable. Поэтому awaitable должен сам заботиться о том, в каком потоке запускать выполнение.

Например, мы отправили retrofit.Call в await, вызовем метод enqueue() и зарегистрируем слушателя. Retrofit сам позаботится, чтобы сетевой запрос был запущен в фоновом потоке.

Для удобства существует один вариант метода await, который принимает функцию () –> R и запускает её в другом потоке:

async, async и asyncUI

Существует три варианта метода async

  • async: ничего не возвращает (как Unit или void)
  • async : возвращает значение типа T
  • asyncUI: ничего не возвращает

При использовании async , необходимо вернуть значение типа T. Сам же метод async возвращает значение типа Task , которое, как вы наверно догадались, можно отправить в метод await:

Более того, метод asyncUI гарантирует, что продолжение(код между await) будет происходит в главном потоке. Если же использовать async или async , то продолжение будет происходить в том же потоке, в котором был вызван callback:

В заключении

Как вы могли заметить, coroutin’ы предоставляют интересные возможности и могут улучшить читаемость кода, если ими пользоваться правильно. Сейчас они доступны в kotlin версии 1.1-M02, а возможности async-await, описанные в этой стате, вы можете использовать с помощью моей библиотеки на github.

Источник

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, 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.

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.

Читайте также:  Sony bravia android tvs

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.

Источник

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