Kotlin coroutines android example

Корутины 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 памяти.

Читайте также:  Дубль гис для андроид ранних версий

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

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

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

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

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

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

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

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

Источник

Coroutines on Android (part I): Getting the background

This is part of a multi-part series about using Coroutines on Android. This post focuses on how coroutines work and what problems they solve.

Coroutines on Android (part II): Getting started

This is part of a multi-part series about using Coroutines on Android. This part focuses on starting work and keeping…

Coroutines On Android (part III): Real work

Learn how to use coroutines to do real work like using a database or making a network request.

What problems do coroutines solve?

Kotlin coroutines introduce a new style of concurrency that can be used on Android to simplify async code. While they’re new to Kotlin in 1.3, the concept of coroutines has been around since the dawn of programming languages. The first language to explore using coroutines was Simula in 1967.

In the last few years, coroutines have grown in popularity and are now included in many popular programming languages such as Javascript, C#, Python, Ruby, and Go to name a few. Kotlin coroutines are based on established concepts that have been used to build large applications.

On Android, coroutines are a great solution to two problems:

  1. Long running tasks are tasks that take too long to block the main thread.
  2. Main-safety allows you to ensure that any suspend function can be called from the main thread.

Let’s dive in to each to see how coroutines can help us structure code in a cleaner way!

Long running tasks

Fetching a webpage or interacting with an API both involve making a network request. Similarly, reading from a database or loading an image from disk involve reading a file. These sorts of things are what I call long running tasks — tasks that take far too long for your app to stop and wait for them!

It can be hard to understand how fast a modern phone executes code compared to a network request. On a Pixel 2, a single CPU cycle takes just under 0.0000000004 seconds, a number that’s pretty hard to grasp in human terms. However, if you think of a network request as one blink of the eye, around 400 milliseconds (0.4 seconds), it’s easier to understand how fast the CPU operates. In one blink of an eye, or a somewhat slow network request, the CPU can execute over one billion cycles!

Читайте также:  Программирование для android или ios

On Android, every app has a main thread that is in charge of handling UI (like drawing views) and coordinating user interactions. If there is too much work happening on this thread, the app appears to hang or slow down, leading to an undesirable user experience. Any long running task should be done without blocking the main thread, so your app doesn’t display what’s called “jank,” like frozen animations, or respond slowly to touch events.

In order to perform a network request off the main thread, a common pattern is callbacks. Callbacks provide a handle to a library that it can use to call back into your code at some future time. With callbacks, fetching developer.android.com might look like this:

Even though get is called from the main thread, it will use another thread to perform the network request. Then, once the result is available from the network, the callback will be called on the main thread. This is a great way to handle long running tasks, and libraries like Retrofit can help you make network requests without blocking the main thread.

Using coroutines for long running tasks

Coroutines are a way to simplify the code used to manage long running tasks like fetchDocs . To explore how coroutines make the code for long running tasks simpler, let’s rewrite the callback example above to use coroutines.

Doesn’t this code block the main thread? How does it return a result from get without waiting for the network request and blocking? It turns out coroutines provide a way for Kotlin to execute this code and never block the main thread.

Coroutines build upon regular functions by adding two new operations. In addition to invoke (or call) and return, coroutines add suspend and resume.

  • suspend — pause the execution of the current coroutine, saving all local variables
  • resume — continue a suspended coroutine from the place it was paused

This functionality is added by Kotlin by the suspend keyword on the function. You can only call suspend functions from other suspend functions, or by using a coroutine builder like launch to start a new coroutine.

Suspend and resume work together to replace callbacks.

In the example above, get will suspend the coroutine before it starts the network request. The function get will still be responsible for running the network request off the main thread. Then, when the network request completes, instead of calling a callback to notify the main thread, it can simply resume the coroutine it suspended.

Looking at how fetchDocs executes, you can see how suspend works. Whenever a coroutine is suspended, the current stack frame (the place that Kotlin uses to keep track of which function is running and its variables) is copied and saved for later. When it resumes, the stack frame is copied back from where it was saved and starts running again. In the middle of the animation — when all of the coroutines on the main thread are suspended, the main thread is free to update the screen and handle user events. Together, suspend and resume replace callbacks. Pretty neat!

When all of the coroutines on the main thread are suspended, the main thread is free to do other work.

Even though we wrote straightforward sequential code that looks exactly like a blocking network request, coroutines will run our code exactly how we want and avoid blocking the main thread!

Next, let’s take a look into how to use coroutines for main-safety and explore dispatchers.

Main-safety with coroutines

In Kotlin coroutines, well written suspend functions are always safe to call from the main thread. No matter what they do, they should always allow any thread to call them.

But, there’s a lot of things we do in our Android apps that are too slow to happen on the main thread. Network requests, parsing JSON, reading or writing from the database, or even just iterating over large lists. Any of these have the potential to run slowly enough to cause user visible “jank” and should run off the main thread.

Using suspend doesn’t tell Kotlin to run a function on a background thread. It’s worth saying clearly and often that coroutines will run on the main thread. In fact, it’s a really good idea to use Dispatchers.Main.immediate when launching a coroutine in response to a UI event — that way, if you don’t end up doing a long running task that requires main-safety, the result can be available in the very next frame for the user.

Coroutines will run on the main thread, and suspend does not mean background.

To make a function that does work that’s too slow for the main thread main-safe, you can tell Kotlin coroutines to perform work on either the Default or IO dispatcher. In Kotlin, all coroutines must run in a dispatcher — even when they’re running on the main thread. Coroutines can suspend themselves, and the dispatcher is the thing that knows how to resume them.

Читайте также:  Infamous second son android

To specify where the coroutines should run, Kotlin provides three Dispatchers you can use for thread dispatch.

** Networking libraries such as Retrofit and Volley manage their own threads and do not require explicit main-safety in your code when used with Kotlin coroutines.

To continue with the example above, let’s use the dispatchers to define the get function. Inside the body of get you call withContext(Dispatchers.IO) to create a block that will run on the IO dispatcher. Any code you put inside that block will always execute on the IO dispatcher. Since withContext is itself a suspend function, it will work using coroutines to provide main safety.

With coroutines you can do thread dispatch with fine-grained control. Because withContext lets you control what thread any line of code executes on without introducing a callback to return the result, you can apply it to very small functions like reading from your database or performing a network request. So a good practice is to use withContext to make sure every function is safe to be called on any Dispatcher including Main — that way the caller never has to think about what thread will be needed to execute the function.

In this example, fetchDocs is executing on the main thread, but can safely call get which performs a network request in the background. Because coroutines support suspend and resume, the coroutine on the main thread will be resumed with the result as soon as the withContext block is complete.

Well written suspend functions are always safe to call from the main thread (or main-safe).

It’s a really good idea to make every suspend function main-safe. If it does anything that touches the disk, network, or even just uses too much CPU, use withContext to make it safe to call from the main thread. This is the pattern that coroutines based libraries like Retrofit and Room follow. If you follow this style throughout your codebase your code will be much simpler and avoid mixing threading concerns with application logic. When followed consistently, coroutines are free to launch on the main thread and make network or database requests with simple code while guaranteeing users won’t see “jank.”

Performance of withContext

withContext is as fast as callbacks or RxJava for providing main safety. It’s even possible to optimize withContext calls beyond what’s possible with callbacks in some situations. If a function will make 10 calls to a database, you can tell Kotlin to switch once in an outer withContext around all 10 calls. Then, even though the database library will call withContext repeatedly, it will stay on the same dispatcher and follow a fast-path. In addition, switching between Dispatchers.Default and Dispatchers.IO is optimized to avoid thread switches whenever possible.

What’s next

In this post we explored what problems coroutines are great at solving. Coroutines are a really old concept in programming languages that have become popular recently due to their ability to make code that interacts with the network simpler.

On Android, you can use them to solve two really common problems:

  1. Simplifying the code for long running tasks such as reading from the network, disk, or even parsing a large JSON result.
  2. Performing precise main-safety to ensure that you never accidentally block the main thread without making code difficult to read and write.

In the next post we’ll explore how they fit in on Android to keep track of all the work you started from a screen! Give it a read:

Источник

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