- Coroutines basics
- Your first coroutine
- Structured concurrency
- Extract function refactoring
- Scope builder
- Scope builder and concurrency
- An explicit job
- Coroutines ARE light-weight
- Котлин корутины. Часть 2. Управление пользовательским интерфейсом
- Добавьте область coroutine scope в MainViewModel
- CoroutineContext
- Отмените область scope, когда ViewModel очищен
- Используйте viewModelScope для избежания boilerplate code
- Переключение с threads на coroutines
- Coroutine Scope
- Convention for structured concurrency
- Android usage
- Custom usage
- Современный подход к конкурентности в Android: корутины в Kotlin
Coroutines basics
This section covers basic coroutine concepts.
Your first coroutine
A coroutine is an instance of suspendable computation. It is conceptually similar to a thread, in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one.
Coroutines can be thought of as light-weight threads, but there is a number of important differences that make their real-life usage very different from threads.
Run the following code to get to your first working coroutine:
You can get the full code here.
You will see the following result:
Let’s dissect what this code does.
launch is a coroutine builder. It launches a new coroutine concurrently with the rest of the code, which continues to work independently. That’s why Hello has been printed first.
delay is a special suspending function. It suspends the coroutine for a specific time. Suspending a coroutine does not block the underlying thread, but allows other coroutines to run and use the underlying thread for their code.
runBlocking is also a coroutine builder that bridges the non-coroutine world of a regular fun main() and the code with coroutines inside of runBlocking < . >curly braces. This is highlighted in an IDE by this: CoroutineScope hint right after the runBlocking opening curly brace.
If you remove or forget runBlocking in this code, you’ll get an error on the launch call, since launch is declared only in the CoroutineScope:
The name of runBlocking means that the thread that runs it (in this case — the main thread) gets blocked for the duration of the call, until all the coroutines inside runBlocking < . >complete their execution. You will often see runBlocking used like that at the very top-level of the application and quite rarely inside the real code, as threads are expensive resources and blocking them is inefficient and is often not desired.
Structured concurrency
Coroutines follow a principle of structured concurrency which means that new coroutines can be only launched in a specific CoroutineScope which delimits the lifetime of the coroutine. The above example shows that runBlocking establishes the corresponding scope and that is why the previous example waits until World! is printed after a second’s delay and only then exits.
In a real application, you will be launching a lot of coroutines. Structured concurrency ensures that they are not lost and do not leak. An outer scope cannot complete until all its children coroutines complete. Structured concurrency also ensures that any errors in the code are properly reported and are never lost.
Extract function refactoring
Let’s extract the block of code inside launch < . >into a separate function. When you perform «Extract function» refactoring on this code, you get a new function with the suspend modifier. This is your first suspending function. Suspending functions can be used inside coroutines just like regular functions, but their additional feature is that they can, in turn, use other suspending functions (like delay in this example) to suspend execution of a coroutine.
You can get the full code here.
Scope builder
In addition to the coroutine scope provided by different builders, it is possible to declare your own scope using the coroutineScope builder. It creates a coroutine scope and does not complete until all launched children complete.
runBlocking and coroutineScope builders may look similar because they both wait for their body and all its children to complete. The main difference is that the runBlocking method blocks the current thread for waiting, while coroutineScope just suspends, releasing the underlying thread for other usages. Because of that difference, runBlocking is a regular function and coroutineScope is a suspending function.
You can use coroutineScope from any suspending function. For example, you can move the concurrent printing of Hello and World into a suspend fun doWorld() function:
You can get the full code here.
This code also prints:
Scope builder and concurrency
A coroutineScope builder can be used inside any suspending function to perform multiple concurrent operations. Let’s launch two concurrent coroutines inside a doWorld suspending function:
You can get the full code here.
Both pieces of code inside launch < . >blocks execute concurrently, with World 1 printed first, after a second from start, and World 2 printed next, after two seconds from start. A coroutineScope in doWorld completes only after both are complete, so doWorld returns and allows Done string to be printed only after that:
An explicit job
A launch coroutine builder returns a Job object that is a handle to the launched coroutine and can be used to explicitly wait for its completion. For example, you can wait for completion of the child coroutine and then print «Done» string:
You can get the full code here.
This code produces:
Coroutines ARE light-weight
Run the following code:
You can get the full code here.
It launches 100K coroutines and, after 5 seconds, each coroutine prints a dot.
Источник
Котлин корутины. Часть 2. Управление пользовательским интерфейсом
В этом упражнении мы напишем корутину для отображения сообщения после задержки. Для начала убедитесь, что у вас есть проект kotlin-coroutines-start, открытый в Android Studio. Скачать проект можно в предыдущей части.
Чтобы на практике увидеть работу с Kotlin Coroutines и архитектурными компонентами, записывайтесь на продвинутый курс по разработке приложения «Чат-мессенжер»
Добавьте область coroutine scope в MainViewModel
В Kotlin все корутины работают внутри CoroutineScope. Область действия контролирует время жизни корутин через свою работу. Когда вы отменяете работу области, она отменяет все корутины , запущенные в этой области. В Android вы можете использовать область действия для отмены всех запущенных корутин, когда, например, пользователь уходит от действия или фрагмента. Области также позволяют указать диспетчер по умолчанию. Диспетчер контролирует, какой поток запускает корутину.
Чтобы запустить корутины в MainViewModel.kt, область действия будет создана следующим образом:
В нашем примере uiScope запустит корутины в Dispatchers.Main, которая является основным потоком в Android. Корутина, запущенная на главном, не будет блокировать главный поток, пока он приостановлен. Поскольку корутина ViewModel почти всегда обновляет пользовательский интерфейс в основном потоке, запуск корутин в основном потоке является разумным значением по умолчанию. Как мы увидим позже в этом коде, корутина может переключать диспетчеры в любое время после ее запуска. Например, корутина может запускаться в главном диспетчере, а затем использовать другой диспетчер для анализа большого JSON ресурса из основного потока.
CoroutineContext
CoroutineScope может принимать CoroutineContext как параметр. CoroutineContext это набор атрибутов, который настраивает корутину. Он может определять политику потоков, обработчик исключений и т. д.
В приведенном выше примере мы используем CoroutineContext плюс оператор для определения политики потоков ( Dispatchers.Main ) и работы ( viewModelJob ). Результирующий CoroutineContext это комбинация обоих контекстов.
Отмените область scope, когда ViewModel очищен
onCleared вызывается, когда ViewModel больше не используется и будет уничтожен. Обычно это происходит, когда пользователь уходит от активити или фрагмента, который использовал ViewModel. Если мы хотим отменить область действия, которую мы видели в предыдущем разделе, вам нужно будет включить следующий код.
Поскольку viewModelJob передается как задание в uiScope, при отмене viewModelJob все корутины, запущенные uiScope, также будут отменены. Важно отменить любые корутины, которые больше не требуются, чтобы избежать ненужной работы и утечек памяти.
Важно: Вы должны передать CoroutineScope Job для отмены всех корутин, запущенных в области. Если вы этого не сделаете, область будет работать, пока ваше приложение не будет завершено. Если это не то, что вы хотели, у вас будет утечка памяти.
Области, созданные с помощью конструктора CoroutineScope, добавляют неявное задание, которое можно отменить с помощью uiScope.coroutineContext.cancel ()
Используйте viewModelScope для избежания boilerplate code
Мы могли бы включить приведенный выше код в каждую ViewModel, имеющуюся в нашем проекте, чтобы привязать к нему область видимости. Тем не менее, это было бы много boilerplate кода. Вот почему мы используем библиотеку AndroidX lifecycle-viewmodel-ktx. Чтобы использовать эту библиотеку, вы должны включить ее в файл build.gradle (Module: app) вашего проекта. Этот шаг уже сделан в проекте, который вы скачали.
Библиотека добавляет viewModelScope в качестве функции расширения класса ViewModel. Эта область привязана к Dispatchers.Main и будет автоматически отменена после очистки ViewModel. Вместо того, чтобы создавать новую область в каждой ViewModel, вы можете просто использовать viewModelScope, а библиотека позаботится о настройке и очистке области за вас.
Вот как вы можете использовать viewModelScope для запуска корутины, которая сделает сетевой запрос в фоновом потоке.
Переключение с threads на coroutines
В MainViewModel.kt найдите следующий TODO вместе с этим кодом:
Этот код использует BACKGROUND для запуска в фоновом потоке. Поскольку sleep блокирует текущий поток, он заморозил бы пользовательский интерфейс, если бы он был вызван в основном потоке. Через одну секунду после того, как пользователь щелкает по main view, он запрашивает снэк-бар.
Замените onMainViewClicked на этот код на основе корутин, который делает то же самое. Вам придется импортировать запуск и задержку.
Этот код делает то же самое, ожидая одну секунду, прежде чем показывать снэк-бар. Тем не менее, есть некоторые важные различия:
- viewModelScope.launch запустит корутину в viewModelScope. Это означает, что когда работа, которую мы передали viewModelScope, будет отменена, все корутины в этой работе / области будут отменены. Если пользователь покинул Activity перед возвратом задержки, эта корутина автоматически отменяется при вызове onCleared после уничтожения ViewModel.
- Поскольку viewModelScope имеет диспетчер по умолчанию Dispatchers.Main, эта корутина будет запущена в главном потоке. Позже мы увидим, как использовать разные потоки.
- Задержка функции является функцией приостановки. Это показано в Android Studio значком
в левой панели. Даже если эта корутина выполняется в основном потоке, задержка не будет блокировать поток на одну секунду. Вместо этого диспетчер запланирует возобновление корутины через одну секунду при следующем вызове.
Запустите приложение. Когда вы щелкнете на главном экране, через секунду вы увидите снэк-бар.
Для отправки комментария вам необходимо авторизоваться.
Источник
Coroutine Scope
Defines a scope for new coroutines. Every coroutine builder (like launch, async, etc.) is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate all its elements and cancellation.
The best ways to obtain a standalone instance of the scope are CoroutineScope() and MainScope() factory functions, taking care to cancel these coroutine scopes when they are no longer needed (see section on custom usage below for explanation and example).
Additional context elements can be appended to the scope using the plus operator.
Convention for structured concurrency
Manual implementation of this interface is not recommended, implementation by delegation should be preferred instead. By convention, the context of a scope should contain an instance of a job to enforce the discipline of structured concurrency with propagation of cancellation.
Every coroutine builder (like launch, async, and others) and every scoping function (like coroutineScope and withContext) provides its own scope with its own Job instance into the inner block of code it runs. By convention, they all wait for all the coroutines inside their block to complete before completing themselves, thus enforcing the structured concurrency. See Job documentation for more details.
Android usage
Android has first-party support for coroutine scope in all entities with the lifecycle. See the corresponding documentation.
Custom usage
CoroutineScope should be declared as a property on entities with a well-defined lifecycle that are responsible for launching children coroutines. The corresponding instance of CoroutineScope shall be created with either CoroutineScope() or MainScope() functions. The difference between them is only in the CoroutineDispatcher:
Источник
Современный подход к конкурентности в Android: корутины в Kotlin
Напоминаем, что у нас уже открыт предзаказ на долгожданную книгу о языке Kotlin из знаменитой серии Big Nerd Ranch Guides. Сегодня мы решили предложить вашему вниманию перевод статьи, рассказывающей о корутинах Kotlin и о правильной работе с потоками в Android. Тема обсуждается очень активно, поэтому для полноты картины также рекомендуем посмотреть эту статью с Хабра и этот подробный пост из блога компании Axmor Software.
Современный фреймворк для обеспечения конкурентности в Java/Android учиняет ад обратных вызовов и приводит к блокирующим состояниям, так как в Android нет достаточно простого способа гарантировать потокобезопасность.
Корутины Kotlin – это очень эффективный и полный инструментарий, позволяющий гораздо проще и производительнее управлять конкурентностью.
Приостановка и блокирование: в чем разница
Корутины не заменяют потоков, а скорее дают фреймворк для управления ими. Философия корутин заключается в определении контекста, позволяющего ожидать, пока завершатся фоновые операции, не блокируя при этом основного потока.
Цель корутин в данном случае – обойтись без обратных вызовов и упростить конкуренцию.
Для начала возьмем самый простой пример: запустим корутину в контексте Main (главный поток). В нем мы извлечем изображение из потока IO и отправим это изображение на обработку обратно в Main .
Код прост как однопоточная функция. Причем, пока getImage выполняется в выделенном пуле потоков IO , главный поток свободен и может взяться за любую другую задачу! Функция withContext приостанавливает текущую корутину, пока работает ее действие ( getImage() ). Как только getImage() возвратится и looper из главного потока станет доступен, корутина возобновит работу в главном потоке и вызовет imageView.setImageBitmap(image) .
Второй пример: теперь нам требуется, чтобы были выполнены 2 фоновые задачи, чтобы ими можно было воспользоваться. Мы применим дуэт async/await, чтобы две эти задачи выполнялись параллельно, и воспользуемся их результатом в главном потоке, как только обе задачи будут готовы:
async подобен launch , но возвращает deferred (сущность Kotlin, эквивалентная Future ), поэтому ее результат можно получить при помощи await() . При вызове без параметров она работает в контексте, задаваемом по умолчанию для текущей области видимости.
Опять же, главный поток остается свободен, пока мы дожидаемся наших 2 значений.
Как видите, функция launch возвращает Job , который можно использовать для ожидания, пока операция завершится – это делается при помощи функции join() . Она работает как и в любом другом языке, с той оговоркой, что просто приостанавливает корутину, а не блокирует поток.
Диспетчеризация – ключевая концепция при работе с корутинами. Это действие, позволяющее «перепрыгнуть» от одного потока к другому.
Рассмотрим, как в java выглядит эквивалент для диспетчеризации в Main , то есть,
Реализация контекста Main для Android – это диспетчер на основе Handler . Итак, это действительно очень подходящая реализация:
launch(Dispatchers.Main) посылает Runnable в Handler , так что его код выполняется не сразу.
launch(Dispatchers.Main, CoroutineStart.UNDISPATCHED) немедленно выполнит свое лямбда-выражение в текущем потоке.
Dispatchers.Main гарантирует, что когда корутина возобновит работу, она будет направлена в главный поток; кроме того, Handler используется здесь как нативная реализация Android для отправки в цикл событий приложения.
Точная реализация выглядит так:
Вот хорошая статья помогающая разобраться в тонкостях диспетчеризации в Android:
Understanding Android Core: Looper, Handler, and HandlerThread.
Контекст корутины (он же – диспетчер корутины) определяет, в каком потоке будет выполняться ее код, что делать, если будет выброшено исключение, и обращается к родительскому контексту для распространения отмены.
job.cancel() отменит все корутины, родителем которых является job . A exceptionHandler получит все исключения, выброшенные в этих корутинах.
Интерфейс coroutineScope упрощает обработку ошибок:
Если откажет какая-либо из его дочерних корутин, то откажет и вся область видимости, и все дочерние корутины будут отменены.
В примере async , если извлечь значение не удалось, а другая задача при этом продолжила работу – у нас возникает поврежденное состояние, и с этим надо что-то делать.
При работе с coroutineScope функция useValues будет вызываться лишь в случае, если извлечение обоих значений прошло успешно. Также, если deferred2 откажет, deferred1 будет отменена.
Также можно “поместить в область видимости” целый класс, чтобы задать для него контекст CoroutineContext по умолчанию и использовать его.
Пример класса, реализующего интерфейс CoroutineScope :
Запуск корутин в CoroutineScope :
Диспетчер launch или async , задаваемый по умолчанию, теперь становится диспетчером актуальной области видимости.
Автономный запуск корутины (вне какого-либо CoroutineScope):
Можно даже определить область видимости для приложения, задав диспетчер Main по умолчанию:
- Корутины ограничивают интероперабельность с Java
- Ограничивают изменяемость во избежание блокировок
- Корутины предназначены для ожидания, а не для организации потоков
- Избегайте I/O в Dispatchers.Default (и Main …) — для этого предназначен Dispatchers.IO
- Потоки ресурсозатратны, поэтому используются однопоточные контексты
- Dispatchers.Default основан на ForkJoinPool , появившемся в Android 5+
- Корутины можно использовать посредством каналов
Избавляемся от блокировок и обратных вызовов при помощи каналов
Определение канала из документации JetBrains:
Канал Channel концептуально очень похож на BlockingQueue . Ключевое отличие заключается в том, что он не блокирует операцию put, он предусматривает приостанавливающий send (или неблокирующий offer ), а вместо блокирования операции take предусматривает приостанавливающий receive .
Рассмотрим простой инструмент для работы с каналами: Actor .
Actor , опять же, очень похож на Handler : мы определяем контекст корутины (то, есть, поток, в котором собираемся выполнять действия) и работаем с ним в последовательном порядке.
Разница, конечно же, заключается в том, что здесь используются корутины; можно указать мощность, а выполняемый код – приостановить.
В принципе, actor будет переадресовывать любую команду каналу корутины. Он гарантирует выполнение команды и ограничивает операции в ее контексте. Такой подход отлично помогает избавиться от вызовов synchronize и держать все потоки свободными!
В данном примере мы пользуемся запечатанными классами Kotlin, выбирая, какое именно действие выполнить.
Причем, все эти действия будут поставлены в очередь, параллельно выполняться они никогда не будут. Это удобный способ добиться ограничения изменяемости.
Жизненный цикл Android + корутины
Акторы могут очень пригодиться и для управления пользовательским интерфейсом Android, упрощают отмену задач и предотвращают перегрузку главного потока.
Давайте это реализуем и вызовем job.cancel() при уничтожении активности.
Класс SupervisorJob похож на обычный Job с тем единственным исключением, что отмена распространяется только в нисходящем направлении.
Поэтому мы не отменяем всех корутин в Activity , когда одна из них отказывает.
Чуть лучше дела обстоят с функцией расширения, позволяющей открыть доступ к этому CoroutineContext из любого View в CoroutineScope .
Теперь мы можем все это скомбинировать, функция setOnClick создает объединенный actor для управления ее действиями onClick . В случае множественных нажатий промежуточные действия будут игнорироваться, исключая таким образом ошибки ANR (приложение не отвечает), и эти действия будут выполняться в области видимости Activity . Поэтому при уничтожении активности все это будет отменено.
В данном примере мы задаем для Channel значение Conflated , чтобы он игнорировал часть событий, если их будет слишком много. Можно заменить его на Channel.UNLIMITED , если вы предпочитаете ставить события в очередь, не теряя ни одного из них, но все равно хотите защитить приложение от ошибки ANR.
Также можно комбинировать корутины и фреймворки Lifecycle, чтобы автоматизировать отмену задач, связанных с пользовательским интерфейсом:
Упрощаем ситуацию с обратными вызовами (часть 1)
Вот как можно преобразить использование API, основанного на обратных вызовах, благодаря Channel .
API работает вот так:
- requestBrowsing(url, listener) инициирует синтаксический разбор папки, расположенной по адресу url .
- Слушатель listener получает onMediaAdded(media: Media) для любого медиа-файла, обнаруженного в этой папке.
- listener.onBrowseEnd() вызывается по завершении синтаксического разбора папки
Вот старая функция refresh в поставщике контента для обозревателя VLC:
Создаем канал, который будет запускаться в refresh . Теперь обратные вызовы обозревателя будут лишь направлять медиа в этот канал, а затем закрывать его.
Теперь функция refresh стала понятнее. Она создает канал, вызывает обозреватель VLC, затем формирует список медиа-файлов и обрабатывает его.
Вместо функций select или consumeEach можно использовать for для ожидания медиа, и этот цикл будет разрываться, как только канал browserChannel закроется.
Упрощаем ситуацию с обратными вызовами (часть 2): Retrofit
Второй подход: мы вообще не используем корутины kotlinx, зато применяем корутинный core-фреймворк.
Смотрите, как на самом деле работают корутины!
Функция retrofitSuspendCall оборачивает запрос на вызов Retrofit Call , чтобы сделать из него функцию suspend .
При помощи suspendCoroutine мы вызываем метод Call.enqueue и приостанавливаем корутину. Предоставленный таким образом обратный вызов обратится к continuation.resume(response) , чтобы возобновить корутину откликом от сервера, как только тот будет получен.
Далее нам остается просто объединить наши функции Retrofit в retrofitSuspendCall , чтобы с их помощью возвращать результаты запросов.
Таким образом, вызов, блокирующий сеть, делается в выделенном потоке Retrofit, корутина находится здесь, ожидая отклика от сервера, а использовать ее в приложении – проще некуда!
Такая реализация вдохновлена библиотекой gildor/kotlin-coroutines-retrofit.
Также имеется JakeWharton/retrofit2-kotlin-coroutines-adapter с другой реализацией, дающей аналогичный результат.
Channel можно использовать и многими другими способами; посмотрите в BroadcastChannel более мощные реализации, которые могут вам пригодиться.
Также можно создавать каналы при помощи функции Produce.
Наконец, при помощи каналов удобно организовать коммуникацию между компонентами UI: адаптер может передавать события нажатий в свой фрагмент/активность через Channel или, например, через Actor .
Источник