Корутины Kotlin позволяют писать понятный и простой асинхронный код, который обеспечивает быстродействие приложения при выполнении длительных задач, таких как сетевые вызовы или операции с диском.
В этом разделе подробно рассматриваются сопрограммы в Android. Если вы не знакомы с сопрограммами, обязательно прочитайте статью Сопрограммы Kotlin в Android, прежде чем продолжить.
Как управлять длительными задачами
Корутины основаны на обычных функциях, но в них добавлены две операции для обработки длительных задач. Помимо invoke (или call) и return, в сопрограммах используются suspend и resume:
suspendприостанавливает выполнение текущей сопрограммы, сохраняя все локальные переменные.resumeпродолжает выполнение приостановленной сопрограммы с того места, где она была приостановлена.
Функции suspend можно вызывать только из других функций suspend или с помощью конструктора сопрограмм, например launch, чтобы запустить новую сопрограмму.
В примере ниже показана простая реализация сопрограммы для гипотетической долго выполняющейся задачи:
suspend fun fetchDocs() { // Dispatchers.Main val result = get("https://developer.android.com") // Dispatchers.IO for `get` show(result) // Dispatchers.Main } suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
В этом примере функция get() по-прежнему выполняется в основном потоке, но приостанавливает работу сопрограммы до начала сетевого запроса. Когда сетевой запрос будет выполнен, get возобновит приостановленную сопрограмму, а не будет использовать обратный вызов для уведомления основного потока.
В Kotlin для управления тем, какая функция выполняется, а также локальными переменными используется стековый фрейм. При приостановке сопрограммы текущий фрейм стека копируется и сохраняется для дальнейшего использования. При возобновлении работы стековый фрейм копируется из места, где он был сохранен, и функция снова начинает выполняться. Хотя код может выглядеть как обычный последовательный блокирующий запрос, сопрограмма гарантирует, что сетевой запрос не будет блокировать основной поток.
Используйте корутины для обеспечения безопасности основного потока
В корутинах Kotlin используются диспетчеры, чтобы определить, какие потоки будут использоваться для выполнения корутин. Чтобы запустить код вне основного потока, можно указать корутинам Kotlin, что работу нужно выполнять в диспетчере Default или IO. В Kotlin все сопрограммы должны выполняться в диспетчере, даже если они работают в основном потоке. Корутины могут приостанавливать свое выполнение, а диспетчер отвечает за их возобновление.
Чтобы указать, где должны выполняться сопрограммы, в Kotlin предусмотрено три диспетчера:
- Dispatchers.Main – используйте этот диспетчер, чтобы запустить сопрограмму в основном потоке Android. Его следует использовать только для взаимодействия с интерфейсом и выполнения простых задач. Например, можно вызывать функции
suspend, выполнять операции с фреймворком Android UI и обновлять объектыLiveData. - Dispatchers.IO – этот диспетчер оптимизирован для выполнения операций ввода-вывода на диске или в сети вне основного потока. Например, это может быть использование компонента Room, чтение или запись файлов, а также выполнение любых сетевых операций.
- Dispatchers.Default – этот диспетчер оптимизирован для выполнения задач, требующих значительных ресурсов ЦП, вне основного потока. Примеры использования: сортировка списка и анализ JSON.
Продолжая предыдущий пример, вы можете использовать диспетчеры, чтобы переопределить функцию get. В теле get вызовите withContext(Dispatchers.IO), чтобы создать блок, который будет выполняться в пуле потоков ввода-вывода. Любой код, помещенный в этот блок, всегда выполняется через диспетчер IO. Поскольку withContext сама является функцией приостановки, функция get также является функцией приостановки.
suspend fun fetchDocs() { // Dispatchers.Main val result = get("developer.android.com") // Dispatchers.Main show(result) // Dispatchers.Main } suspend fun get(url: String) = // Dispatchers.Main withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block) /* perform network IO here */ // Dispatchers.IO (main-safety block) } // Dispatchers.Main
С помощью сопрограмм можно отправлять потоки с точным управлением. Поскольку
withContext() позволяет управлять пулом потоков любой строки кода без использования обратных вызовов, его можно применять к очень небольшим функциям, например для чтения из базы данных или выполнения сетевого запроса. Рекомендуем использовать withContext(), чтобы убедиться, что каждая функция безопасна для основного потока, то есть ее можно вызывать из основного потока. Таким образом, вызывающему коду не нужно думать о том, какой поток следует использовать для выполнения функции.
В предыдущем примере метод fetchDocs() выполняется в основном потоке, но он может безопасно вызывать метод get, который выполняет сетевой запрос в фоновом режиме.
Поскольку сопрограммы поддерживают suspend и resume, сопрограмма в основном потоке возобновляется с результатом get, как только блок withContext будет выполнен.
Производительность функции withContext()
withContext() не создает дополнительной нагрузки по сравнению с эквивалентной реализацией на основе обратного вызова. Кроме того, в некоторых случаях можно оптимизировать вызовы withContext() лучше, чем эквивалентную реализацию на основе обратного вызова. Например, если функция десять раз обращается к сети, вы можете указать Kotlin переключать потоки только один раз, используя внешний withContext(). В результате, даже если сетевая библиотека использует withContext() несколько раз, она остается в том же диспетчере и избегает переключения потоков. Кроме того, Kotlin оптимизирует переключение между Dispatchers.Default и Dispatchers.IO, чтобы по возможности избежать переключения потоков.
Как запустить сопрограмму
Запустить сопрограмму можно двумя способами:
launchзапускает новую сопрограмму и не возвращает результат вызывающей функции. Любую задачу, которую можно запустить и забыть, можно начать с помощьюlaunch.asyncзапускает новую сопрограмму и позволяет вернуть результат с помощью функции приостановкиawait.
Обычно новую сопрограмму следует launch из обычной функции, поскольку обычная функция не может вызывать await. Используйте async только в другой сопрограмме или в функции suspend при параллельном деконструировании.
Параллельное разложение
Все сопрограммы, запущенные внутри функции suspend, должны быть остановлены, когда функция возвращает значение. Поэтому вам, скорее всего, нужно будет убедиться, что эти сопрограммы завершены до возврата. Структурированная параллельность в Kotlin позволяет определить coroutineScope, который запускает одну или несколько сопрограмм. Затем, используя await() (для одной сопрограммы) или awaitAll() (для нескольких сопрограмм), вы можете гарантировать, что эти сопрограммы завершатся до возврата из функции.
Например, определим coroutineScope, который асинхронно получает два документа. Вызывая await() для каждой отложенной ссылки, мы гарантируем, что обе операции async будут завершены до возврата значения:
suspend fun fetchTwoDocs() = coroutineScope { val deferredOne = async { fetchDoc(1) } val deferredTwo = async { fetchDoc(2) } deferredOne.await() deferredTwo.await() }
Вы также можете использовать awaitAll() в подборках, как показано в следующем примере:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main) coroutineScope { val deferreds = listOf( // fetch two docs at the same time async { fetchDoc(1) }, // async returns a result for the first doc async { fetchDoc(2) } // async returns a result for the second doc ) deferreds.awaitAll() // use awaitAll to wait for both network requests }
Хотя fetchTwoDocs() запускает новые сопрограммы с помощью async, функция использует awaitAll(), чтобы дождаться завершения запущенных сопрограмм, прежде чем вернуть результат. Однако даже если бы мы не вызывали awaitAll(), конструктор coroutineScope не возобновил бы работу сопрограммы, вызвавшей fetchTwoDocs, пока не завершатся все новые сопрограммы.
Кроме того, coroutineScope перехватывает все исключения, которые генерируют сопрограммы, и возвращает их вызывающей стороне.
Подробнее о параллельном разложении…
Основные понятия о сопрограммах
CoroutineScope
CoroutineScope отслеживает все созданные им сопрограммы с помощью launch или async. Текущую работу (то есть выполняющиеся сопрограммы) можно отменить, вызвав scope.cancel() в любой момент времени. В Android некоторые библиотеки KTX предоставляют собственные CoroutineScope для определенных классов жизненного цикла. Например, у ViewModel есть viewModelScope, а у Lifecycle – lifecycleScope.
Однако, в отличие от диспетчера, CoroutineScope не запускает сопрограммы.
viewModelScope также используется в примерах из статьи Фоновые потоки в Android с помощью сопрограмм.
Однако, если вам нужно создать собственный CoroutineScope, чтобы управлять жизненным циклом сопрограмм на определенном уровне приложения, вы можете сделать это следующим образом:
class ExampleClass { // Job and Dispatcher are combined into a CoroutineContext which // will be discussed shortly val scope = CoroutineScope(Job() + Dispatchers.Main) fun exampleMethod() { // Starts a new coroutine within the scope scope.launch { // New coroutine that can call suspend functions fetchDocs() } } fun cleanUp() { // Cancel the scope to cancel ongoing coroutines work scope.cancel() } }
Отмененная область не может создавать больше сопрограмм. Поэтому вызывать scope.cancel() следует только при уничтожении класса, который управляет его жизненным циклом. При использовании viewModelScope класс ViewModel автоматически отменяет область действия в методе onCleared() ViewModel.
Задание
Job – это дескриптор сопрограммы. Каждая созданная вами сопрограмма с помощью launch или async возвращает экземпляр Job, который уникальным образом идентифицирует сопрограмму и управляет ее жизненным циклом. Вы также можете передать Job в CoroutineScope, чтобы управлять его жизненным циклом, как показано в следующем примере:
class ExampleClass { // ... fun exampleMethod() { // Handle to the coroutine, you can control its lifecycle val job = scope.launch { // New coroutine } if (condition) { // Cancel the coroutine started above, this doesn't affect the scope // this coroutine was launched in job.cancel() } } }
CoroutineContext
CoroutineContext определяет поведение сопрограммы с помощью следующих элементов:
Job: Управляет жизненным циклом сопрограммы.CoroutineDispatcher: отправляет работу в нужный поток.CoroutineName: Название сопрограммы, полезное для отладки.CoroutineExceptionHandler: Обрабатывает необработанные исключения.
Для новых сопрограмм, созданных в области действия, новому экземпляру Job присваивается новая сопрограмма, а другие элементы CoroutineContext наследуются от содержащей области действия. Вы можете переопределить унаследованные элементы, передав новый объект CoroutineContext в функцию launch или async. Обратите внимание, что передача Job в launch или async не имеет никакого эффекта, поскольку новому сопрограмме всегда назначается новый экземпляр Job.
class ExampleClass { val scope = CoroutineScope(Job() + Dispatchers.Main) fun exampleMethod() { // Starts a new coroutine on Dispatchers.Main as it's the scope's default val job1 = scope.launch { // New coroutine with CoroutineName = "coroutine" (default) } // Starts a new coroutine on Dispatchers.Default val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) { // New coroutine with CoroutineName = "BackgroundCoroutine" (overridden) } } }
Дополнительные ресурсы о сопрограммах
Дополнительную информацию о сопрограммах можно найти по ссылкам ниже.