Корутины и асинхронность

Введение в корутины

Последнее обновление: 08.04.2024

При первом запуске приложения Android среда выполнения создает один поток, в котором по умолчанию будут выполняться все компоненты приложения. Этот поток обычно называют основным потоком. Основная роль основного потока — обработка пользовательского интерфейса, его событий и взаимодействие с элементами интерфейса. Любые дополнительные компоненты, запускаемые в приложении, по умолчанию также будут выполняться в основном потоке.

Любой код внутри приложения, который выполняет какую-нибудь трудоемкую задачу с использованием основного потока, приведет к тому, что все приложение заблокируется до тех пор, пока задача не будет завершена. То есть грубо говоря приложение зависает. Это далеко нелучшая ситуация, которая отрицательно влияет на впечателение пользователя от взаимодействия с приложением. И чтобы избавиться от подобных ситуаций мы можем использовать в приложении корутины. Более подробно про корутины можно посмотреть на этом же сайте metanit.com в основном руководстве по языку Kotlin в главе Корутины. В данной же статье мы вкратце затронем все основные моменты, связанные с корутинами.

Корутины представляют блоки кода, которые выполняются асинхронно и не блокируют поток, из которого они запускаются. Корутины не эквивалентны потокам, хотя и используют их. Проблема с потоками заключается в том, что они представляют собой ограниченный ресурс и являются дорогостоящими с точки зрения возможностей процессора и накладных расходов системы. В фоновом режиме выполняется большой объем работы по созданию, планированию и уничтожению потока. Хотя современные процессоры могут выполнять большое количество потоков, фактическое количество потоков, которые могут выполняться параллельно в любой момент времени, ограничено количеством ядер процессора (хотя новые процессоры имеют 8 или более ядер, большинство устройств Android содержат процессоры с 4 ядрами). А когда требуется больше потоков, чем имеется ядер в процессоре, система должна выполнить планирование потоков, чтобы решить, как выполнение этих потоков будет распределяться между доступными ядрами.

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

Все корутины должны выполняться в определенной области (scope), что позволяет управлять ими как группами, а не как отдельными корутинами. Назначив для некоторой области группу корутин, эту группу можно массово отменить, когда ее корутины больше не нужны.

Kotlin и Android предоставляют некоторые встроенные области корутин (scope), а также возможность создавать собственные области корутин с помощью класса CoroutineScope. Основные встроенные области корутин:

  • GlobalScope: используется для запуска корутин верхнего уровня, которые привязаны ко всему жизненному циклу приложения. Поскольку корутины в этой области могут продолжать работать, когда в этом нет необходимости (например, когда объект Activity завершает свою работу), использование этой области не рекомендуется для использования в приложениях Android.

  • ViewModelScope: применяется при использовании компонента ViewModel архитектуры Jetpack. Корутины, запущенные в этой области из объекта ViewModel, автоматически завершаются системой при уничтожении соответствующего объекта ViewModel.

  • LifecycleScope: создается для каждого компонента с жизненным циклом и удаляется, когда уничтожается соответствующий компонент.

Самый простой способ определить область действия корутин внутри компонента состоит в вызове функции rememberCoroutineScope()

val coroutineScope = rememberCoroutineScope()

Объект coroutineScope определяет область действия корутин. Он объявляет диспетчер, который будет использоваться для запуска корутин, а также применяется для включения корутин в область видимости. Все запущенные в этой области корутины можно завершить с помощью вызова метода cancel():

coroutineScope.cancel()

Для определения корутин применяется особый тип функций Kotlin - suspend-функции. Подобные функции объявляются с помощью ключевого слова suspend, которое указывает Kotlin, что функцию можно приостановить и возобновить позже, что позволяет выполнять длительные вычисления без блокировки основного потока. Например:

suspend fun doWork() {

     // Здесь выполняем длительную задачу

}

Диспетчеры корутин

Диспетчер отвечает за назначение корутин определенным потокам, а также за приостановку и возобновление работы корутины в течение ее жизненного цикла. Kotlin поддерживает потоки для различных типов асинхронной активности, и при запуске корутины можно указать конкретного диспетчера из следующих вариантов:

  • Dispatchers.Main: запускает корутину в основном потоке и подходит для корутин, которым надо взаимодействовать с пользовательским интерфейсом, а также в качестве универсального варианта для выполнения легких задач.

  • Dispatchers.IO: рекомендуется для корутин, которые выполняют операции ввода-вывода - операции с сетью, диском или базой данных.

  • Dispatchers.Default: предназначен для задач, которые требуют интенсивного использования процессора, таких как сортировка данных или выполнение сложных вычислений.

Например, следующий код запускает корутину с помощью диспетчера ввода-вывода:

coroutineScope.launch(Dispatchers.IO) {

    // выполнение операций с сетью, базой данных или файлами

}

Строители корутин

Строители корутин (coroutine builders) создают и запускают корутины. Kotlin предоставляет следующие типы строителей корутин:

  • launch: запускает корутину, не блокируя текущий поток, и не возвращает результат вызывающей стороне. Вызывается из suspend-функции и применяется когда не требуются результаты корутины

  • async: запускает корутину и позволяет вызывающей стороне дождаться результата с помощью функции await(), не блокируя текущий поток. Применяется для параллельного запуска нескольких корутин. Вызывается только из другой suspend-функции.

  • withContext: позволяет запускать корутину в контексте, отличном от того, который используется родительской корутиной

  • coroutineScope: подходит для ситуаций, когда suspend-функция запускает несколько корутин, которые будут выполняться параллельно, и когда некоторые действия должны выполняться только тогда, когда все корутины завершаются. Если эти корутины запускаются с помощью построителя coroutineScope, вызывающая функция не завершится до тех пор, пока не завершатся все дочерние корутины. При использовании coroutineScope сбой в любой из корутин приведет к отмене всех остальных корутин.

  • supervisorScope: аналогичен coroutineScope за исключением того, что сбой в одном дочернем элементе не приводит к отмене других корутин.

  • runBlocking: запускает корутину и блокирует текущий поток до тех пор, пока корутина не завершится. Обычно это противоположно тому, что требуется от корутин, но полезно для тестирования кода и интеграции устаревшего кода и библиотек.

Job

Каждый вызов построителя корутин возвращает объект Job, который можно использовать для отслеживания и управления жизненным циклом соответствующей корутины. Последующие вызовы построителя корутины из уже запущенной корутины создают новые объекты Job, которые станут дочерними элементами родительского объекта Job, образуя дерево отношений родитель-потомок. А отмена родительского объекта Job рекурсивно отменит все его дочерние элементы Job. Однако отмена дочернего элемента не отменяет родителя.

Тип Job обладает рдядом полезных свойств и методов. Свойства isActive, isCompleted и isCancelled связанного объекта Job позволяют определить статус корутины. Метод cancel() завершает объект Job и все дочерние корутины, а вызов метода cancelChildren() завершит только все дочерние корутины. Метод join() можно вызвать для приостановки корутины до тех пор, пока не будут выполнены все дочерние объекты Job. Чтобы выполнить объект Job и завершить его после всех дочерних заданий, вызывается метод cancelAndJoin().

Вызов корутин к Compose

Рассмотрим пример применения корутин:

package com.example.helloapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.ui.unit.sp
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent{
            val coroutineScope = rememberCoroutineScope()
            Column(Modifier.padding(5.dp).fillMaxSize()) {
                Button(onClick = {
                    coroutineScope.launch {
                        doWork()
                    }
                }) {
                    Text("Click", fontSize = 28.sp)
                }
            }
        }
    }
}
suspend fun doWork() {
    println("doWork starts")
    delay(5000) // симулируем долгую работу с помощью задержки в 5 секунд
    println("doWork ends")
}

Здесь определена suspend-функция doWork, которая просто выводит на консоль пару сообщений. Для симуляции долговременной работы в ней вызывается функция delay(), которая устанавливает задержку в 5 секунд. Но в реальности это могло бы быть обращение к локальным файлам, к базе данных, сетевые запросы или какие-нибудь долговременные вычисления.

Для вызова этой функции создается область корутины:

val coroutineScope = rememberCoroutineScope()

В компоненте Button определяем код обработчика нажатия - в нем запускается корутина:

Button(onClick = {
    coroutineScope.launch {
        doWork()
    }
})

Для создания корутины здесь применяется построитель coroutineScope.launch, и в области корутины вызывается функция doWork.

Запустим приложение и нажмем на кнопку, и в окне Logcat внизу Android Studio мы сможем увидеть сообщения из функции doWork:

Корутины и асинхронность в Jetpack Compose и Kotlin на Android

Аналогичным образом можно обращаться из корутин к состоянию компонентов и таким образом воздействовать на компоненты:

package com.example.helloapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.ui.unit.sp
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent{
            val coroutineScope = rememberCoroutineScope()
            val enabled = remember{mutableStateOf(true)}
            val count = remember{mutableStateOf(0)}
            Column(Modifier.padding(5.dp).fillMaxSize()) {
                Text("Count: ${count.value}", Modifier.padding(start = 4.dp), fontSize = 28.sp)
                Button(onClick = {
                    coroutineScope.launch {
                        enabled.value = false
                        for(n in 1..5){
                            count.value = n
                            delay(1000)
                        }
                        enabled.value = true
                    }
                }, enabled = enabled.value) {
                    Text("Start", fontSize = 28.sp)
                }
            }
        }
    }
}

Здесь при нажати на кнопку корутина изменяет значения переменных состояния enabled и count. В данном случае при нажатии на кнопку делаем кнопку недоступной для нажатия, покак не завершит работу корутина. Для этого переключаем значение переменной enabled, которая привязана к одноименному параметру кнопки Button. В корутине в цикле увеличиваем значение count до 5 и затем завершаем корутину, делая кнопку внось активной. А для вывода текущего значения count определен компонент Text.

Корутины и пользовательский интерфейс в Jetpack Compose и Kotlin на Android
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850