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

1. Что такое «корутины» в Kotlin, чем они отличаются от потоков?

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

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

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

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

Таким образом, конкурентное программирование реализуется потоками и корутинами, но последние легковеснее, эффективнее и предсказуемее в написании асинхронного кода Kotlin. У корутин выше уровень абстракции для управления конкурентностью, особенно хороши они для асинхронных задач в разработке Android.

2. Объясните преимущества использования корутин в разработке Android

Это существенные преимущества:

  1. Асинхронное программирование. С корутинами просто и интуитивно понятно писать асинхронный код: выполнять длительные задачи, такие как сетевые запросы, операции с базами данных или файловый ввод-вывод, не блокируя основной поток. Так поддерживается адаптивный пользовательский интерфейс, предотвращаются ANR-ошибки.
  2. Лаконичный, удобный для восприятия код. С корутинами асинхронный код пишется в последовательной манере, как синхронный. В итоге он получается чище, удобнее для восприятия и сопровождения по сравнению с традиционными структурами на основе обратных вызовов или вложенными структурами асинхронного кода. Эта четкость достигается в горутинах благодаря приостанавливающим функциям и структурированной конкурентности.
  3. Легкость и эффективность. В отличие от потоков, то есть тяжелых ресурсов операционной системы, корутины легковесны и запускаются сотнями или тысячами без больших накладных расходов. Поэтому корутинами эффективнее расходуются ресурсы, особенно на мобильных устройствах, где ресурсы ограничены.
  4. Структурированная конкурентность. Благодаря ее поддержке в корутинах, параллельно выполняемые задачи организовываются и управляются в иерархическом порядке. Так корутины корректно распределяются на области и контролируются, за счет чего предотвращается утечка ресурсов, упрощаются обработка ошибок и отмена корутин.
  5. Легкая интеграция с функционалом Kotlin  —  приостанавливающими функциями, построителями и областями корутин. Поэтому разработчикам здесь несложно освоить корутины и асинхронное программирование в целом.
  6. Совместимость с имеющимися асинхронными API в Android: LiveData, Room, Retrofit и другими. Они легко интегрируются с кодовыми базами, серьезных архитектурных изменений при этом не требуется. Поэтому корутины проще адаптировать как в новых, так и в имеющихся проектах.
  7. Поддержка тестирования асинхронного кода. Благодаря ей написание модульных и интеграционных тестов с корутинами упрощается. В библиотеках вроде kotlinx-coroutines-test имеются утилиты для тестирования  —  с надежным тестовым покрытием  —  приостанавливающих функций и кода на основе корутин.

В целом корутины  —  это современный, эффективный подход к асинхронному программированию в разработке на Android, с которым код получается чище, адаптивнее, проще в сопровождении. Ими устраняются многие проблемы, связанные с моделями традиционного асинхронного программирования, предоставляется упрощенное решение для конкурентного выполнения в приложениях Android.

3. Как создается корутина в Kotlin? Назовите способы ее запуска

В Kotlin корутины создаются и запускаются их построителями корутин, в основном это функции launch, async и runBlocking.

В launch запускается новая корутина, которой асинхронно выполняется задача и возвращается объект Job  —  сама корутина.

Вот пример, как создать и запустить корутину с launch:

import kotlinx.coroutines.*

fun main() {
// Запускаем новую корутину
val job = GlobalScope.launch {
// Тело корутины
delay(1000) // Моделируем работу
println("Coroutine is running")
}

// Ожидаем завершения корутины
runBlocking {
job.join()
}
}

В async запускается новая корутина, которой асинхронно выполняется задача и возвращается результат  —  объект Deferred.

Вот пример, как создать и запустить корутину с async:

import kotlinx.coroutines.*

fun main() {
// Запускаем новую корутину
val deferred = GlobalScope.async {
// Тело корутины
delay(1000) // Моделируем работу
"Result from coroutine"
}

// Ожидаем завершения корутины и получаем результат
runBlocking {
val result = deferred.await()
println("Coroutine result: $result")
}
}

В runBlocking запускается новая корутина, которой блокируется текущий поток, пока она не завершится, и возвращается ее результат.

Вот пример, как создать и запустить корутину с runBlocking:

import kotlinx.coroutines.*

fun main() {
// Запускаем новую корутину
val result = runBlocking {
// Тело корутины
delay(1000) // Моделируем работу
"Result from coroutine"
}

// Используем результат корутины
println("Coroutine result: $result")
}

У каждого построителя корутин имеются собственные варианты использования и поведение, подбирайте под свои требования.

4. Чем отличаются launch, async и runBlocking в корутинах?

В основном вариантами использования и возвращаемыми значениями:

В launch запускается новая корутина, которой асинхронно выполняется задача без возвращения результата, возвращается объект Job  —  сама корутина для управления и контроля за ее жизненным циклом, например отмены или ожидания завершения корутины.

launch обычно применяется для задач по принципу «запустил и забыл» без получения результата из корутины, таких как выполнение фоновой работы или обновление элементов пользовательского интерфейса.

В async запускается новая корутина, которой асинхронно выполняется задача и возвращается результат  —  объект Deferred для получения результата после завершения корутины.

async обычно применяется для выполнения фоновой задачи и извлечения ее результата, например получения данных из сети или выполнения асинхронных вычислений с интенсивным расходом ресурсов процессора.

В runBlocking запускается новая корутина, которой блокируется текущий поток, пока она не завершится, и возвращается ее результат с возможностью выполнить код корутины в условиях блокировки вне контекста корутины, например функции main или модульного теста.

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

В итоге:

  • launch для задач по принципу «запустил и забыл» без получения результата.
  • async для асинхронного выполнения задачи и получения ее результата.
  • runBlocking в основном для написания тестового кода или выполнения кода корутины в условиях блокировки вне контекстов корутины. Во избежание блокировки основного потока не используется в производственном коде.

5. Объясните концепцию области корутины. Как управлять областью корутины в приложениях на Android?

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

В приложениях Android, чтобы обеспечивать корректное выполнение асинхронных задач и предотвращать утечки памяти, важно управлять областью корутины. Вот как это делается:

GlobalScope

GlobalScope  —  это предопределенная область корутины, доступная глобально во всем приложении. Запущенные в ней корутины не привязаны к жизненному циклу конкретного компонента, например Activity или Fragment, и продолжают выполняться, пока не отменятся явно.

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

CoroutineScope

CoroutineScope  —  это пользовательская область корутины, обычно связанная с жизненным циклом конкретного компонента, например Activity или Fragment.

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

Область корутины создается конструктором CoroutineScope с указанием контекста корутины , например MainScope, IOCoroutineScope, которым определяется контекст выполнения для запускаемых в этой области корутин.

LifecycleScope

LifecycleScope  —  это предопределенная область корутины из библиотеки AndroidX Lifecycle, привязанная к жизненному циклу конкретного компонента, например Activity или Fragment. Когда компонент уничтожается или больше не находится в активном состоянии, корутины автоматически отменяются этой областью.

Доступ к LifecycleScope получается через свойство lifecycleScope, указанное в артефакте lifecycle-runtime-ktx.

Вот пример управления областью корутины в приложении Android с LifecycleScope:

import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MyActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// В жизненном цикле «activity» запускаем корутину
lifecycleScope.launch {
// Тело корутины
delay(1000) // Моделируем работу
println("Coroutine is running")
}
}
}

Таким образом, чтобы корректно управлять жизненным циклом корутины и предотвращать утечки памяти, в приложениях Android важно управлять областью корутины. Связыванием корутин с жизненным циклом компонентов и применением соответствующих областей корутин, например LifecycleScope, создается надежный и эффективный асинхронный код, легко интегрируемый с платформой Android.

6. Что такое «приостанавливающие функции» в корутинах? Чем они отличаются от обычных?

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

Вот чем приостанавливающие функции отличаются от обычных.

Приостановка выполнения

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

Они неблокирующие

Приостанавливающие функции  —  неблокирующие, то есть во время ожидания завершения приостановленной операции вызывающий поток ими не блокируется. Так конкурентные приостанавливающие функции выполняются параллельно, не расходуя дополнительных потоков.

Контекст корутины

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

Область корутины

Приостанавливающими функциями получается доступ к области и контексту корутины, в котором они выполняются. В контексте корутины ими указывается контекст выполнения, например пул потоков или диспетчер, для приостановленной операции.

Корутиноцентричное программирование

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

Интеграция с построителями корутин

Асинхронные задачи в корутинах часто выполняются приостанавливающими функциями с помощью построителей корутин launch и async. Этими построителями удобно запускать корутины, которыми выполняются приостанавливающие функции и обрабатываются их приостановка и возобновление.

7. Как в корутинах обрабатываются исключения?

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

Блоками «try/catch»
В корутинах исключения перехватываются локально обычными блоками try/catch:

import kotlinx.coroutines.*

fun main() {
runBlocking {
launch {
try {
// Код с возможностью выбрасывания исключения
throw RuntimeException("An error occurred")
} catch (e: Exception) {
println("Caught exception: ${e.message}")
}
}
}
}

CoroutineExceptionHandler
Необработанные исключения перехватываются в корутинах глобальным обработчиком исключений:

import kotlinx.coroutines.*

fun main() {
val exceptionHandler = CoroutineExceptionHandler { _, exception ->
println("Caught unhandled exception: ${exception.message}")
}

runBlocking {
val job = launch(exceptionHandler) {
// Код с возможностью выбрасывания исключения
throw RuntimeException("An error occurred")
}
job.join()
}
}

SupervisorJob
При использовании заданий супервизора исключения в одной дочерней корутине не сказываются на других:

import kotlinx.coroutines.*

fun main() {
val supervisor = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + supervisor)

scope.launch {
try {
// Код с возможностью выбрасывания исключения
throw RuntimeException("An error occurred")
} catch (e: Exception) {
println("Caught exception: ${e.message}")
}
}

scope.launch {
// Другая корутина
}

// Перед выходом все корутины обязательно завершаются
scope.joinAll()
}

CoroutineExceptionHandler из CoroutineScope
При создании CoroutineScope указывается обработчик CoroutineExceptionHandler для перехвата исключений, выбрасываемых всеми запущенными в области корутинами. Вот пример:

import kotlinx.coroutines.*

fun main() {
val scope = CoroutineScope(Dispatchers.Default + CoroutineExceptionHandler { _, exception ->
println("Caught unhandled exception: ${exception.message}")
})

scope.launch {
// Код с возможностью выбрасывания исключения
throw RuntimeException("An error occurred")
}

// Перед выходом все корутины обязательно завершаются
scope.joinAll()
}

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

8. Объясните концепцию контекста корутины и диспетчеров. Назовите самые распространенные в разработке Android диспетчеры

Контекст корутины и диспетчеры  —  важные понятия, которыми определяются контекст выполнения и поведение корутин Kotlin. Рассмотрим каждое из них:

Контекст корутины
Это набор специфичных для корутины элементов, которыми определяются ее поведение и контекст выполнения: диспетчер, задание, название и другие контекстные элементы.

  • Контекст корутины неизменяем, он корректируется построителями корутин и операторами: launch, async, withContext, CoroutineScope.
  • Контекст корутины  —  это такой набор правил и параметров, которыми определяются ее поведение и место выполнения.

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

В Kotlin имеются встроенные диспетчеры:

  • Dispatchers.Default: оптимизирован для вычислений или задач с интенсивным расходом ресурсов процессора, использованием общего пула фоновых потоков.
  • Dispatchers.IO: оптимизирован для задач с ограничением скорости ввода-вывода, таких как сетевые запросы, операции с файлами, доступ к базе данных, для параллельного выполнения конкурентных операций ввода-вывода им используется общий пул фоновых потоков.
  • Dispatchers.Main: специфичен для разработки Android, представляет основной поток или поток ПИ, то есть пользовательского интерфейса, используется в операциях ПИ, таких как обновление элементов ПИ или обработка пользовательского ввода.
  • Dispatchers.Unconfined: корутины запускаются им без конкретных ограничений в любом потоке, в том числе вызывающем, для разработки Android обычно не рекомендуется из-за непредсказуемого поведения.

В разработке Android чаще применяются Dispatchers.IO и Dispatchers.Main. Первый для задач с ограничением скорости ввода-вывода: сетевые запросы, файловые операции, доступ к базе данных  —  вне основного потока, чтобы не блокировать ПИ. Второй для операций ПИ: обновление элементов ПИ, обработка пользовательского ввода  —  в основном потоке, чтобы обеспечить потокобезопасность и адаптивность ПИ.

Применяя контекст корутины и диспетчеры для эффективного контроля ее поведения и контекста выполнения в приложении Android, вы обеспечиваете адаптивное асинхронное программирование.

9. Как с корутинами в Android выполняются фоновые задачи?

Для этого запускаются корутины с диспетчерами. Вот пошаговое руководство.

Добавление зависимостей корутин
Необходимые зависимости включаются в файл build.gradle проекта Android:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x'

Меняем x.x.x на используемую версию корутин Kotlin.

Запуск корутины с диспетчером ввода-вывода
Построителем launch запускаем корутину для выполнения фоновых задач. Чтобы задача выполнялась в фоновом потоке, внутри корутины указываем Dispatchers.IO:

import kotlinx.coroutines.*

fun performBackgroundTask() {
CoroutineScope(Dispatchers.IO).launch {
// Фоновая задача, например сетевой запрос, операции с файлами
// ...
}
}

Выполнение фоновой задачи
Внутри корутины выполняем фоновую задачу: сетевой запрос, доступ к базе данных, считывание файла  —  асинхронно, не блокируя корутину, с помощью приостанавливающих функций из библиотек Retrofit, Room или стандартной библиотеки Kotlin:

import kotlinx.coroutines.*

fun performBackgroundTask() {
CoroutineScope(Dispatchers.IO).launch {
val result = fetchDataFromNetwork() // Пример приостанавливающей функции
// Результат обрабатываем
// ...
}
}

suspend fun fetchDataFromNetwork(): String {
// Выполняем сетевой запрос, например, с помощью Retrofit
// ...
return "Data from network"
}

Обновление ПИ в основном потоке (необязательно)
Чтобы обновить ПИ результатом фоновой задачи, переключаемся на Dispatchers.Main с помощью функции withContext:

import kotlinx.coroutines.*

fun performBackgroundTaskAndUpdateUI() {
CoroutineScope(Dispatchers.IO).launch {
val result = fetchDataFromNetwork() // Пример приостанавливающей функции
withContext(Dispatchers.Main) {
// Обновляем ПИ результатом
// ...
}
}
}

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

10. Как в Kotlin отменяются корутины? Опишите лучшие практики отмены корутин в приложениях Android

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

Вот как отменяют корутины согласно лучшим практикам для приложений Android:

Вызовом функции cancel() в объекте Job, так корутина отменяется со всеми дочерними корутинами:

val job = CoroutineScope(Dispatchers.Default).launch {
// Тело корутины
}

// Отменяем корутину
job.cancel()

Отменой области CoroutineScope мы отменяем все запущенные в ней корутины:

val scope = CoroutineScope(Dispatchers.Default)
val job1 = scope.launch {
// Корутина 1
}
val job2 = scope.launch {
// Корутина 2
}

// Отменяем все запущенные в области корутины
scope.cancel()

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

val supervisor = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + supervisor)
val job1 = scope.launch {
// Корутина 1
}
val job2 = scope.launch {
// Корутина 2
}

// Отменяем только одну из дочерних корутин
job1.cancel()

Корректной обработкой отмены в корутинах. Отменена ли корутина, проверяем в ее свойстве isActive:

val job = CoroutineScope(Dispatchers.Default).launch {
while (isActive) {
// Выполняем задачу
}
}

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

class MyViewModel : ViewModel() {
private val viewModelScope = CoroutineScope(Dispatchers.Main)

fun performBackgroundTask() {
viewModelScope.launch {
// Тело корутины
}
}

override fun onCleared() {
super.onCleared()
viewModelScope.cancel() // Отменяем все корутины, когда «ViewModel» очищен
}
}

Обработкой исключений CancellationException отмены корутины, выбрасываемых во время выполнения приостанавливающей функции, они обрабатываются блоками try/catch или вызовом функции resumeWithException() из CancellableContinuation:

val job = CoroutineScope(Dispatchers.Default).launch {
try {
// Выполняем задачу
} catch (e: CancellationException) {
// Обрабатываем отмену
}
}

Так, следуя этим лучшим практикам, в приложении Android эффективно отменяют корутины и управляют ресурсами, поддерживая производительность и предотвращая утечки памяти.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Rizwanul Haque: Important Coroutine Interview Questions for Experienced Android Developers

Предыдущая статьяВыполняйте загрузку Excel-файлов в Python в 1000 раз быстрее
Следующая статьяПриемы работы в терминале Linux для повышения продуктивности