Coroutines
https://kotlinlang.org/docs/coroutines-guide.html |
21.01.2023 | https://youtu.be/ITLe4FIrrTg |
28.09.2022 | https://youtu.be/L04cpMbNQ10 |
24.05.2022 | https://youtu.be/arUctP5yAYc |
24.05.2022 | https://www.youtube.com/playlist?list=PL0SwNXKJbuNmsKQW9mtTSxNn00oJlYOLA |
27.08.2024 | https://habr.com/ru/articles/838974/ |
11.07.2024 | https://habr.com/ru/articles/827866/ |
Coroutines
Легковесные асинхронные конструкции, которые позволяют выполнять операции параллельно и эффективно управлять асинхронным кодом. Они являются частью языка Kotlin и позволяют писать асинхронный код, который выглядит и ведёт себя как синхронный, что упрощает чтение и поддержку кода.
Structures cuncurrency
Концепция, которая обеспечивает предсказуемое и безопасное управление жизненным циклом корутин. Она позволяет организовать корутины в иерархическую структуру, что упрощает обработку ошибок и отмену задач.
• Иерархия корутин: Все корутины должны быть запущены в контексте родительской корутины. Это позволяет отслеживать и управлять ими в рамках одного логического блока кода.
• Обработка ошибок: Ошибки, возникшие в дочерних корутинах, могут быть переданы родительской корутине, что упрощает обработку исключений. Это позволяет избежать ситуации, когда ошибки остаются незамеченными.
• Автоматическая отмена: Если родительская корутина отменяется, все дочерние корутины также автоматически отменяются. Это предотвращает выполнение долгих или нежелательных операций, если они больше не нужны.
• Избежание утечек памяти: Структурированная конкуренция помогает избежать утечек памяти, так как корутины будут отменены вместе с их родителями, когда те будут уничтожены.
fun main() = runBlocking {
launch {
// Родительская корутина
val childJob = launch {
// Дочерняя корутина
delay(2000)
println("Дочерняя корутина завершена")
}
// Имитация задержки для родительской корутины
delay(1000)
println("Родительская корутина завершена, дочерняя корутина будет отменена")
childJob.cancel() // Отмена дочерней корутины
}
}
Синхронизация
Mutex
Механизм синхронизации, который позволяет управлять доступом к разделяемым ресурсам между корутинами. Он блокирует доступ к ресурсу, пока один поток или корутина его не освободит.
val mutex = Mutex()
suspend fun criticalSection() {
mutex.withLock {
// Код, который должен быть выполнен эксклюзивно
}
}
newSingleThreadContext
Вручную запускает поток с указанным именем. API деликатное, использовать осторожно.
launch(newSingleThreadContext("Custom Thread")) {
println("coroutine flow: ${Thread.currentThread().name}")"}
newFixedThreadPoolContext
Позволяет создать собственный отдельный пул потоков. Все корутины по умолчанию выполняются в CommonPool
. Все ресурсы будут освобождены после того как программа отработает. API деликатное, использовать осторожно.
val pool: ExecutorCoroutineDispatcher = newFixedThreadPoolContext(8, "myPool")
Builders
launch
Функция, запускающая новую корутину в заданном контексте, которая не блокирует текущий поток и возвращает объект Job
, позволяющий управлять выполнением корутины. Выстрелил и забыл.
// корутина запускается в рамках runBlocking и выполняется асинхронно, а job.join() ожидает её завершения.
fun main() = runBlocking {
val job = launch {
// Код корутины
delay(1000)
println("Hello from coroutine!")
}
job.join() // Ожидание завершения корутины
}
async
Функция, которая запускает новую корутину и возвращает объект Deferred
, позволяющий получить результат выполнения корутины в будущем. Позволяет использовать функцию await()
для получения результата, который будет доступен после завершения корутины.
// async запускает корутину, возвращающую значение 42, и мы используем await() для получения этого значения после её завершения.
fun main() = runBlocking {
val deferred: Deferred<Int> = async {
// Код корутины, возвращающей результат
delay(1000)
42
}
val result = deferred.await() // Ожидание завершения корутины и получение результата
println("The result is $result")
}
await
Используется в корутинах для ожидания завершения корутины, запущенной с помощью async
.
awaitAll
Позволяет параллельно ожидать завершения нескольких корутин и собрать их результаты. Она используется для того, чтобы запустить несколько асинхронных операций и дождаться их всех одновременно.
fun main() = runBlocking {
val result = awaitAll(
async { fetchData1() },
async { fetchData2() }
)
println("Results: $result")
}
suspend fun fetchData1(): String {
delay(1000)
return "Data 1"
}
suspend fun fetchData2(): String {
delay(1500)
return "Data 2"
}
Deferred
Представляет собой задачу, результат которой будет доступен в будущем. Это значение, которое ещё не вычислено, но будет доступно после завершения асинхронной операции. Обычно создаётся с помощью функции async
.
Context
CoroutineContext
Определяет поведение корутины. Является набором параметров для выполнения корутин.
• Каждая корутина выполняется в каком-либо контексте.
• Явно не создается, задается в scope
либо при запуске корутины (в launch
).
• Можно объединить несколько контекстов в один.
withContext
Переносит выполнение текущей корутины на новый контекст, в большинстве случаев на новый диспетчер.
NonCancellable
Специальный объект, который используется для создания контекста корутины, который нельзя отменить. Когда корутина запускается с контекстом NonCancellable
, она не реагирует на попытки отмены, пока находится в этом контексте. Основной случай использования — выполнение завершающих операций, таких как очистка ресурсов или сохранение данных, которые должны быть выполнены даже при отмене корутины.
launch {
try {
// Выполнение основного действия, которое можно отменить
} finally {
withContext(NonCancellable) {
// Код, который не должен быть отменен
println("Завершение ресурса")
}
}
}
Scope
CoroutineScope
Отслеживает любую корутину, которую создает, используя launch
или async
. Scope хранит все ссылки на корутины, запущенные в нем. Scope может отменить выполнение всех дочерних корутин, если возникнет ошибка или операция будет отменена.
• Отмена дочернего scope приведет к отмене родительского scope и наоборот.
• Scope будет завершен успешно, когда выполнятся все корутины в нем.
• Если запустить бесконечный цикл внутри scope и отменить его - цикл закончит выполнение.
supervisorScope
Аналог coroutineScope
. SupervisorJob
для scope. Переопределяет Job
контекст, поэтому функция не отменяется, когда дочерний элемент выдает исключение.
supervisorScope {
...
}
withTimeout
Устанавливает ограничение по времени для выполнения работы. Если время вышло - корутина отменится и вызовется TimeoutCancellationException
.
withTimeout(1_000L) {
...
}
withTimeoutOrNull
Устанавливает ограничение по времени для выполнения работы. Если время вышло вернет результат null.
val result = withTimeoutOrNull(1_000L) {
...
}
MainScope
Предоставляет корутины, которые запускаются в главном потоке (UI-потоке) приложения. Он упрощает управление корутинами, особенно в Android-приложениях, где необходимо взаимодействовать с пользовательским интерфейсом. Создается с использованием ContextScope(SupervisorJob() + Dispatchers.Main)
.
val mainScope = MainScope()
fun fetchData() {
mainScope.launch {
// Здесь можно выполнять асинхронные задачи, работающие с UI
val value = loadData()
updateUI(value)
}
}
GlobalScope
Глобальный контекст, который позволяет запускать корутины, которые будут работать независимо от жизненного цикла конкретных компонентов (например, Activity
в Android). Корутины живут на протяжении всего времени работы приложения.
fun main() {
// Запускаем корутину в GlobalScope
GlobalScope.launch {
// Имитация длительной фоновой задачи
delay(1000)
println("Hello from GlobalScope!")
}
// Ожидание завершения программы, чтобы корутина успела выполниться
Thread.sleep(1500)
}
lifecycleScope
Расширение для корутин, привязанное к жизненному циклу компонента, такого как Activity
или Fragment
. Он автоматически управляет запуском и отменой корутин в зависимости от состояния жизненного цикла компонента, что помогает избежать утечек памяти и ошибок, связанных с обращением к уничтоженным компонентам.
class MainActivity: AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
val result = fetchData()
updateUI(result)
}
}
}
class ExampleFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val result = fetchData()
updateUI(result)
}
}
}
viewModelScope
Специальный диспетчер, который связан с жизненным циклом ViewModel
. Он позволяет запускать корутины, которые будут автоматически отменены, когда ViewModel
будет уничтожена. Это делает viewModelScope
отличным выбором для выполнения асинхронных операций, связанных с обработкой данных в ViewModel
.
class ExampleViewModel: ViewModel() {
fun fetchData() {
viewModelScope.launch {
val result = fetchDataFromNetwork()
}
}
}
rememberCoroutineScope
Функция, которая позволяет получить доступ к CoroutineScope
, связанному с текущим состоянием пользовательского интерфейса. Это полезно для запуска корутин в рамках Composable
-функций, не нарушая принципов управления состоянием в Compose.
@Composable
fun ExampleScreen() {
val coroutineScope = rememberCoroutineScope()
Button(onClick = {
coroutineScope.launch {
val result = fetchData()
println("Результат: $result")
}
}) {
Text("Загрузить данные")
}
}
Dispatchers
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/limited-parallelism.html |
Dispatcher определяет, на каком потоке или пуле потоков будет выполняться код корутины. Он управляет распределением задач между потоками и позволяет выбирать подходящий контекст для выполнения операций.
Dispatcher.Default
Предназначен для выполнения задач, требующих значительных вычислительных ресурсов, таких как обработка данных или выполнение сложных алгоритмов. Он использует пул потоков, оптимизированный для CPU-ориентированных задач.
• Использует пул потоков, размер которого обычно соответствует числу доступных процессоров (или ядер).
• Не рекомендуется для I/O операций так как использует пул с ограниченным числом потоков, блокировка одного из этих потоков может снизить общую производительность приложения, особенно если таких операций много.
• Используется по умолчанию в launch
и async
.
• Под капотом FixedThreadPool
.
fun main() = runBlocking {
val result = withContext(Dispatchers.Default) {
// Выполнение ресурсоёмкой операции
computeIntensiveTask()
}
println("Результат вычислений: $result")
}
Dispatcher.IO
Используется для выполнения блокирующих операций ввода-вывода, таких как работа с файлами или сетевые запросы. Он оптимизирован для работы с большим количеством фоновых задач, распределяя их по пулу потоков.
• Пул потоков имеет динамический размер. Количество потоков может увеличиваться в зависимости от текущей нагрузки и количества ожидающих задач.
• Если все потоки заняты, новые задачи помещаются в очередь и обрабатываются по мере освобождения потоков.
• Под капотом CachedThreadPool
.
launch(Dispatchers.IO) {
// Выполнение сетевого запроса
val result = fetchDataFromNetwork()
println("Результат сетевого запроса: $result")
}
Dispatcher.Main
Специальный диспетчер, который использует главный поток Android (или основной поток в других платформах). Он предназначен для выполнения задач, связанных с пользовательским интерфейсом, таких как обновление UI элементов, обработка событий и другие задачи, которые должны быть выполнены в главном потоке.
coroutineScope.launch(Dispatchers.Main) {
// Этот код выполнится в главном потоке
textView.text = "Обновлено на главном потоке"
}
Dispatcher.Main.immediate
Оптимизированная версия диспетчера Dispatchers.Main
, которая позволяет корутине сразу выполняться на главном потоке, если она уже запущена на нём. Это снижает накладные расходы на переключение контекста, что особенно полезно для задач, требующих немедленного выполнения на UI-потоке.
coroutineScope.launch(Dispatchers.Main.immediate) {
// Выполнится немедленно, если уже на главном потоке
textView.text = "Hello, World!"
}
Dispatchers.Unconfined
Запускает корутину в текущем потоке, но не привязан к какому-либо конкретному потоку. Это означает, что корутина может продолжить выполнение в другом потоке после первой приостановки.
coroutineScope.launch(Dispatchers.Unconfined) {
println("Запущено в потоке: ${Thread.currentThread().name}")
delay(1000)
println("Возобновлено в потоке: ${Thread.currentThread().name}")
}
limitedParallelism
Позволяет ограничить количество параллельно выполняемых корутин в данном CoroutineDispatcher
. Он создает новый CoroutineDispatcher
, который использует исходный, но ограничивает количество параллельно работающих корутин до указанного значения.
• Например, если у вас есть пул потоков с 4 потоками, и вы хотите, чтобы только 2 корутины обрабатывали изображения одновременно, вы можете использовать limitedParallelism(2)
.
• Это полезно для случаев, когда определенные задачи требуют ограничения параллелизма, чтобы избежать перегрузки ресурсов, например при выполнении CPU-интенсивных операций.
// Диспетчер фоновых задач для приложения
val dispatcher = newFixedThreadPoolContext(4, "App Background")
// Максимум 2 потока будут обрабатывать изображения, так как это очень медленная и ресурсоемкая задача
val imageProcessingDispatcher = dispatcher.limitedParallelism(2, "Image processor")
// Максимум 3 потока будут обрабатывать JSON, чтобы избежать блокировки потоков для обработки изображений
val jsonProcessingDispatcher = dispatcher.limitedParallelism(3, "Json processor")
// Максимум 1 поток будет выполнять операции ввода-вывода (IO)
val fileWriterDispatcher = dispatcher.limitedParallelism(1, "File writer")
Exceptions
CancellationException
Исключение, которое указывает, что корутина была отменена. Оно выбрасывается автоматически при вызове функции cancel()
или отмене корутины через родительский элемент, если у них общая иерархия контекстов.
Когда корутина отменяется, выполнение её кода прекращается, но блоки finally и catch могут быть выполнены для очистки ресурсов или завершения работы. CancellationException
не рассматривается как ошибка — это нормальный способ завершения работы корутины.
val job = launch {
try {
// Действия корутины
} catch (e: CancellationException) {
println("Корутина отменена: ${e.message}")
} finally {
println("Очистка ресурсов")
}
}
// Отмена корутины
job.cancel(CancellationException("Причина отмены"))
CoroutineExceptionHandler
Интерфейс, который используется для обработки необработанных исключений в корутинах. Он позволяет задать кастомную логику для обработки исключений, которые возникают в корутине и не были пойманы. Его можно передать в контекст корутины и перехватывать только необработанные исключения в корутинах, запущенных через launch
(в async
исключения обрабатываются через метод await
).
val handler = CoroutineExceptionHandler { _, exception ->
println("Исключение поймано: ${exception.localizedMessage}")
}
val job = GlobalScope.launch(handler) {
throw RuntimeException("Ошибка в корутине")
}
Job
Основной механизм управления корутинами в Kotlin, который позволяет эффективно управлять жизненным циклом корутин, их отменой и композицией.
• Когда вы запускаете корутину с помощью функций launch или async, возвращается объект Job
, который представляет корутину.
• На основе Job
можно организовать иерархию parent-child.
fun main() = runBlocking {
val job: Job = launch {
// Код корутины
delay(1000)
println("Coroutine finished")
}
// Ожидание завершения корутины
job.join()
println("Main program finished")
}
SupervisorJob
Специальный тип Job
, который используется для создания корутин, не зависимых друг от друга. В отличие от обычного Job
, который отменяет все дочерние корутины при отмене или ошибке одной из них, SupervisorJob
позволяет дочерним корутинам продолжать работу, даже если одна из них завершится с ошибкой.
fun main() = runBlocking {
val supervisor = SupervisorJob()
val scope = CoroutineScope(Dispatchers.Default + supervisor)
val child1 = scope.launch {
println("Child 1 started")
throw RuntimeException("Error in Child 1")
}
val child2 = scope.launch {
println("Child 2 started")
delay(1000)
println("Child 2 finished")
}
child1.join() // Ожидание завершения Child 1
child2.join() // Ожидание завершения Child 2
println("Main program finished")
}
Lifecycle
• New
Корутина создана, но ещё не запущена.
• Active
Корутина запущена и выполняется.
• Completing
Корутина завершается.
• Completed
Корутина успешно завершилась без ошибок.
• Cancelling
Корутина отменяется.
• Canceled
Корутина была отменена до завершения (вызван метод cancel
у Job
)
start
Запускает корутину, если она ещё не запущена.
val job = GlobalScope.launch { /* корутина */ }
job.start() // Явный запуск корутины
join
Блокирует текущий поток пока корутина не завершится.
val job = GlobalScope.launch { /* корутина */ }
job.join() // Ожидание завершения корутины
joinAll
Блокирует текущий поток пока не завершится несколько корутин.
fun main() = runBlocking {
val job1 = launch { delay(1000); println("Job 1 done") }
val job2 = launch { delay(2000); println("Job 2 done") }
// Ожидаем завершения обеих корутин
joinAll(job1, job2)
println("All jobs done")
}
cancel
Отменяет выполнение корутины.
val job = GlobalScope.launch { /* корутина */ }
job.cancel() // Отменяет корутину
cancelAndJoin
Отменяет выполнение корутины и ожидает её завершения.
val job = GlobalScope.launch { /* корутина */ }
job.cancelAndJoin() // Отмена корутины и ожидание её завершения
isActive
Проверяет, активна ли корутина (т.е., она запущена и не отменена).
val job = GlobalScope.launch { /* корутина */ }
if (job.isActive) { /* корутина активна */ }
isCancelled
Проверяет, была ли корутина отменена.
val job = GlobalScope.launch { /* корутина */ }
if (job.isCancelled) { /* корутина отменена */ }
isCompleted
Проверяет, завершена ли корутина (успешно или из-за ошибки).
val job = GlobalScope.launch { /* корутина */ }
if (job.isCompleted) { /* корутина завершена */ }
invokeOnCompletion
Устанавливает обработчик, который будет вызван, когда корутина завершится.
val job = GlobalScope.launch { /* корутина */ }
job.invokeOnCompletion {
// Код, выполняемый после завершения корутины
}
suspend
Используется для обозначения функций, которые могут быть приостановлены и возобновлены в будущем. Такие функции могут выполнять асинхронные задачи и могут вызываться только из других suspend
-функций или корутин. Это позволяет эффективно управлять асинхронными операциями без блокировки потоков. Такие функции могут использовать функции-расширения корутин, например, delay
, withContext
и другие, для работы с асинхронными задачами.
// Определение suspend-функции
suspend fun doWork() {
delay(1000L) // Приостанавливает выполнение на 1 секунду
println("Work done!")
}
fun main() = runBlocking { // Запускает корутину
launch {
doWork() // Вызов suspend-функции внутри корутины
}
println("Starting work...")
}
Continuation
Когда suspend
-функция вызывается, она получает объект Continuation
, который управляет состоянием функции и её продолжением. Continuation
хранит информацию о том, где нужно продолжить выполнение после приостановки.
При выполнении suspend
-функции, выполнение может быть приостановлено в точке, где происходит вызов другой приостановленной функции или задержка (delay
). Этот процесс преобразует функцию в корутину, которая работает асинхронно и может быть возобновлена позже.
Состояние функции (например, локальные переменные и текущее положение) сохраняется в объекте Continuation
. Когда функция приостанавливается, состояние сохраняется, и выполнение возобновляется с этого состояния, когда корутина продолжает выполнение.
Когда suspend функция возобновляется, объект Continuation
используется для продолжения выполнения с точки, на которой функция была приостановлена. Это позволяет функции продолжить выполнение, как если бы она никогда не была приостановлена.
suspendCoroutine
Позволяет интегрировать некорутинный код (например, использующий коллбэки) с корутинами.
// Пример callback hell — когда асинхронные вызовы вложены друг в друга, что делает код сложным для чтения и сопровождения
fun fetchData(callback: (String) -> Unit) {
getNetworkData { result1 ->
getDatabaseData(result1) { result2 ->
processResult(result2) { finalResult ->
callback(finalResult)
}
}
}
}
// Пример того, как можно обернуть колбэки с помощью suspendCoroutine
suspend fun fetchData(): String = suspendCoroutine { continuation ->
getNetworkData { result1 ->
getDatabaseData(result1) { result2 ->
processResult(result2) { finalResult ->
// Возвращаем результат через continuation.resume
continuation.resume(finalResult)
}
}
}
}
// Идеальное решение
suspend fun fetchData(): String {
val networkData = getNetworkDataAsync()
val databaseData = getDatabaseDataAsync(networkData)
return processResultAsync(databaseData)
}
runBlocking
Специальная корутинная функция, предназначенная для запуска корутин в синхронном контексте. Она блокирует текущий поток до завершения выполнения корутин, что делает её полезной в ситуациях, когда требуется дождаться выполнения асинхронных операций перед продолжением работы.
• Блокировка потока: runBlocking
запускает новую корутину и блокирует текущий поток до тех пор, пока все вложенные корутины не завершат свою работу. Это позволяет использовать корутины в обычных (не корутинных) функциях.
• Контекст: runBlocking
принимает контекст корутины, который определяет диспетчер и другие параметры выполнения. По умолчанию он использует CoroutineScope
, созданный с Dispatchers.Default
.
• Использование: runBlocking
обычно используется в тестах или в функции main
, где необходимо выполнить корутины, но вы не хотите, чтобы основной поток продолжал выполняться, пока они не завершатся.
fun main() = runBlocking {
launch {
delay(1000)
println("World!")
}
println("Hello,")
}
runCatching
Функция, которая используется для выполнения блока кода и обработки возможных исключений. Она возвращает результат выполнения блока в виде объекта Result
, который упрощает работу с результатами и ошибками. Позволяет избежать использования конструкции try-catch
. Предоставляет методы, такие как getOrNull()
и getOrElse()
, для удобного извлечения результата или обработки ошибки.
fun main() {
// Выполнение блока кода и получение результата в виде объекта Result
val result: Result<Int> = runCatching {
// Код, который может вызвать исключение
"123".toInt()
}
// Получение результата или обработки ошибки
val value: Int? = result.getOrNull() // Возвращает значение, если не было исключения
val error: Throwable? = result.exceptionOrNull() // Возвращает исключение, если оно произошло
println("Value: $value") // Выводит: Value: 123
println("Error: $error") // Выводит: Error: null (поскольку исключение не произошло)
}
CoroutineName
Позволяет назначить уникальное имя корутине для удобства отладки. Используется для улучшения читаемости логов и диагностики, помогая легче отслеживать корутины в процессе их выполнения. Отображается в Android Studio Profiler. Не влияет на логику выполнения корутины и не изменяет ее поведение.
// Создание корутины с именем
val job = CoroutineScope(Dispatchers.Default + CoroutineName("MyCoroutine")).launch {
// Your coroutine code here
}
// При создании вложенных корутин имя будет унаследовано от родительской корутины.
val job = CoroutineScope(Dispatchers.Default + CoroutineName("ParentCoroutine")).launch {
launch(CoroutineName("ChildCoroutine")) {
// Child coroutine code here
}
}
yield
Приостанавливает выполнение текущей корутины, чтобы дать возможность другим корутинам выполниться. Это полезно для кооперативной многозадачности, когда вы хотите, чтобы текущая корутина уступила время выполнения другим корутинам на том же Dispatcher
. После приостановки корутина может быть возобновлена в дальнейшем.
suspend fun doWork() {
for (i in 1..5) {
println("Working on task $i")
yield() // Приостановка, чтобы дать шанс другим корутинам выполниться
}
}
ensureActive
Проверяет, активна ли корутина (не была ли отменена), и выбрасывает исключение CancellationException
, если корутина была отменена. Этот оператор полезен, когда вы хотите явным образом проверить, не отменили ли текущую корутину, чтобы прервать её выполнение безопасно.
suspend fun doWork() {
for (i in 1..5) {
ensureActive() // Проверка, не отменена ли корутина
println("Working on task $i")
}
}
delay
Приостанавливает выполнение корутины, освобождая поток на время задержки. При этом диспетчер корутин использует системные таймеры для отслеживания времени, и по его истечении корутина возобновляется. Это не блокирующая операция, и поток может использоваться для других задач.
fun main() = runBlocking {
println("Start")
launch {
delay(1000) // Приостановит корутину на 1 секунду
println("Executed after 1 second")
}
println("End")
}
suspendCancellableCoroutine
Позволяет превратить асинхронный вызов в приостанавливаемую функцию с поддержкой отмены. Она передает блоку управления Continuation
, который можно завершить или отменить.
suspend fun fetchData(): String = suspendCancellableCoroutine { continuation ->
val callback = object: Callback {
override fun onSuccess(result: String) {
continuation.resume(result) // Завершает с результатом
}
override fun onError(e: Throwable) {
continuation.resumeWithException(e) // Завершает с ошибкой
}
}
Api.request(callback)
continuation.invokeOnCancellation {
Api.cancelRequest() // Обрабатывает отмену
}
}
Вопросы на собесе (85)
Обработка ошибок (5)
- Как обрабатывать исключения в корутинах?
• С помощью
try-catch
.•
CoroutineExceptionHandler
для необработанных исключений.•
SupervisorJob
для изоляции исключений в дочерних корутинах.
- Что нужно учитывать при обработки ошибок через try-catch в корутинах?
При обработке ошибок через
try-catch
в корутинах нужно учитывать, что исключения (CancellationException
), возникшие в корутинах, могут отменить корутину, иtry-catch
работает только в пределах текущего контекста корутины.
- Если передать CoroutineExceptionHandler в async, как обработается исключение?
Корутина выполнится, исключение будет в
Deferred
.
- Разница в обработке ошибок в launch и async?
В
launch
ошибки не возвращаются вызывающей функции и обрабатываются в корутине, в то время как в async исключения выбрасываются при вызовеawait()
, что позволяет обрабатывать их в месте вызова.
- Что произойдет в обоих случаях, на каком этапе будет исключение?
fun someFun1() { val a: Int = async { suspendFun) } } fun someFun2() { launch { suspendFun() } } private fun suspendFun(): Int { throw RuntimeException) }
• В someFun1 исключение будет при вызове
await
в объектеDeferred
.• В someFun2 исключение будет при запуске
launch
.
- Как обрабатывать исключения в корутинах?
Отмена корутин (9)
- Как отменить корутину?
Методом
cancel()
.
- Перечисли все способы отменить корутину?
•
Job.cancel()
.•
Job.cancelChildren()
.•
Job.cancelAndJoin()
.•
Scope.cancel()
.
- Как гарантировано остановить корутину, будет ли достаточно метода cancel?
Нет, метода
cancel
недостаточно для гарантированной остановки корутины. Он лишь устанавливает флаг отмены, и корутина должна регулярно проверять этот флаг с помощью функцииisActive
или выбрасыватьCancellationException
. Это позволяет корректно завершать выполнение, освобождая ресурсы. Для полного завершения рекомендуется использоватьjoin
послеcancel
, чтобы дождаться завершения корутины.
- Какое поведение у корутины при вызове метода cancel?
При вызове метода
cancel
корутина помечается какCancelled
, и её выполнение прекращается при следующем проверке на отмену.
- Как корутина понимает что она отменена?
Корутина понимает, что она отменена, когда проверяет состояние своей
Job
или когда происходит вызов методаisActive
. Отмена сигнализируется выбросом исключенияCancellationException
, которое корутина может поймать и обработать, если это предусмотрено.
- Почему не рекомендуется перехватывать CancellationException?
Перехват
CancellationException
не рекомендуется, поскольку он является индикатором нормальной отмены корутины, и его игнорирование может привести к неявным ошибкам, затруднить управление ресурсами и нарушить ожидаемую логику отмены.
- Как работает кооперативность отмены в корутинах?
Кооперативная отмена в корутинах позволяет корутинам проверять статус отмены и завершать выполнение в заранее определенных точках, что обеспечивает освобождение ресурсов и корректное завершение работы. Отмена происходит при вызове метода
cancel()
на родительской корутине или с помощьюwithContext
и других конструкций, поддерживающих отмену.
- Как отменить все корутины но не отменить Scope?
Использовать
cancelChildren()
.
- Что произойдет при вызове данной функции?
suspend fun unknownFunction() { coroutineScope { launch(Job()) { delay(1000) println("Hello!") } cancel() } }
• Запустится корутина
launch
, после задержки в 1 секунду, будет выведено "Hello!", потом скоуп отменится.• Запустится корутина
launch
, scope будет сразу же отменен, отменится корутинаlaunch
, ничего выведено не будет.• Запустится корутина
launch
, scope будет сразу же отменен, однако корутинаlaunch
продолжит свою работу, через секунду будет выведено «Hello!».• Нет правильного ответа.
- Как отменить корутину?
Builders (9)
- Как запустить корутину? (Какие существуют билдеры корутин)
С помощью билдеров корутин
launch
илиasync
.
- Разница между launch и async?
launch
запускает корутину без возврата результата и возвращаетJob
, в то время какasync
также запускает корутину, но возвращаетDeferred
, который используется для получения результата выполнения.
- Что возвращают launch и async?
•
launch
возвращает объектJob
.•
async
возвращает объектDeferred
.
- Как дождаться выполнения launch?
Использовать метод
join()
. Он приостановит выполнение текущей корутины до завершения корутины, запущенной через launch.
- Как запустить 2 задачи параллельно?
Использовать
launch
fun main() { val job1 = launch { task1() } val job2 = launch { task2() } // Ожидаем завершения обеих задач job1.join() job2.join() }
- Как запустить 2 задачи параллельно и дождаться результата?
Использовать
async
fun main() { val result1 = async { task1() } val result2 = async { task2() } val combinedResult = result1.await() + result2.await() }
- Как дождаться результата, если 1 из 2 корутин вернула исключение?
Использовать
supervisorScope
, который позволяет одной задаче завершиться с исключением, не влияя на другие.
- В каком случае выполнение будет параллельным, а в каком последовательным?
fun sum1(): Int { val a = async { suspendFun() }.await() val b = async { suspendFun() }.await() return a + b }
fun sum2(): Int { val a = async { suspendFun() } val b = async { suspendFun() } return a.await() + b.await() }
• sum1 последовательно.
• sum2 параллельно.
- Что выведет функция?
suspend fun startTest() { val first = scope.async { delay(100) println("1") "First Async" } val second = scope.async { delay(400) println("2") "Second Async" } delay(300) println("3") println("4 - ${first.await()} ${second.await()}") }
1
3
2
4 - First Async Second Async
- Как запустить корутину? (Какие существуют билдеры корутин)
Dispatchers (15)
- Что такое диспетчер корутины?
Диспетчер корутины управляет потоком выполнения корутины, определяя, на каком пуле потоков будет выполняться код.
- Какие диспетчеры есть у корутин?
Dispatchers.Default
Dispatchers.IO
Dispatchers.Main
Dispatcher.Main.immediate
Dispatchers.Unconfined
- Для чего используются каждый из диспетчеров?
•
Dispatchers.Main
для работы с UI, обновления интерфейса.•
Dispatchers.Main.immediate
для работы с UI, немедленного обновления интерфейса.•
Dispatchers.IO
для операций ввода-вывода (сеть, файлы).•
Dispatchers.Default
для вычислительно тяжелых задач.•
Dispatchers.Unconfined
выполняет в текущем потоке до первой приостановки, потом переключается в зависимости от возобновления.
- Какой объем пула потоков на каждом из диспетчеров?
•
Dispatchers.Main
использует главный поток (UI) без пула потоков.•
Dispatchers.Main.immediate
использует главный поток (UI) без пула потоков.•
Dispatchers.IO
имеет динамический пул потоков, по умолчанию до 64 потоков, расширяется при необходимости для операций ввода-вывода.•
Dispatchers.Default
использует пул потоков, равный количеству доступных ядер процессора.•
Dispatchers.Unconfined
не привязан к какому-либо конкретному пулу потоков, выполняется в текущем потоке до приостановки.
- Разница между Dispatcher.Default и Dispathcer.IO?
Dispatcher.Default
оптимизирован для CPU-интенсивных задач и использует пул потоков равный количеству доступных ядер процессора, аDispatcher.IO
предназначен для I/O операций и имеет динамический пул потоков.
- Что произойдет с корутиной при переключении с Dispatcher.Default на Dispathcer.IO?
Если диспетчеры используют разные потоки, то фактически корутина будет перенесена на новый поток. Это происходит за счет переноса контекста и выполнения задачи на нужном пуле потоков. Переключение между диспетчерами требует переключения контекста, что может быть дорогой операцией, так как это связано с изменением потоков и управлением контекстом выполнения. Однако это меньше затраты по сравнению с созданием нового потока или корутины с нуля.
- Изменится ли реальный поток выполнения при смене диспетчера через withContext?
И да и нет. Корутины используют общий пул путоков, например для
Default
иIO
.
- Почему в Default можно выполнить только 4 операции одновременно (для 4 ядер), а в IO 64?
Из-за псевдопараллельности. Современные процессоры могут содержать специализированные сетевые модули (Network Processing Units, NPUs или Network Interface Controllers, NICs), которые ускоряют обработку сетевых операций. Эти модули снимают часть нагрузки с основных ядер процессора, обрабатывая сетевые запросы более эффективно.
- Чем Dispatchers.Main отличается от Dispatchers.Main.immediate?
Dispatchers.Main
выполняет корутины в основном потоке, планируя их выполнение в очереди, что может привести к задержкам.Dispatchers.Main.immediate
немедленно выполняет корутины в текущем потоке, если они запускаются из основного потока, что может снизить задержки, но также требует осторожности, чтобы избежать блокировки.
- Зачем нужен Dispatchers.Main, если в Android везде используется Dispatchers.Main.immediate?
Для других платформ.
- Задача выполняется 1 сек, что выполнится быстрее: 10 тредов в цикле или 10 корутин в Dispatcher.Main?
10 тредов быстрее, потому что в
Dispatcher.Main
1 поток.
- Что будет если выполнять сетевые запросы на Dispatcher.Default?
Если выполнять сетевые запросы на
Dispatcher.Default
, это может привести к блокированию потоков, так какDispatcher.Default
предназначен для CPU-интенсивных операций, а не для I/O задач.
- На каких диспетчерах выполнится код?
withContext(Dispatchers.Main) { val singleValue = intFlow .map { ... } // В каком диспетчере? .flowOn(Dispatchers.IO) .filter { ... } // В каком диспетчере? .flowOn(Dispatchers.Default) .single() // А здесь? }
•
map
— выполнится наDispatchers.IO
, так какflowOn
устанавливает диспетчер для всех операций до него.•
filter
— выполнится наDispatchers.Default
, так как второйflowOn
переключает диспетчер для всех операций до него и после map.•
single
— выполнится наDispatchers.Main
, так как это терминальная операция, и она возвращается к диспетчеру, установленному с помощьюwithContext(Dispatchers.Main)
.
- Каких диспатчеров не существует?
•
Default
• Computation
•
IO
•
Unconfined
• Single
- Какой тип coroutine контекста предназначен для задач, требующих значительных вычислительных ресурсов?
•
Dispatchers.IO
•
Dispatchers.Computation
•
Dispatchers.Main
•
Dispatchers.Default
- Что такое диспетчер корутины?
Job (3)
- Для чего нужен Job?
Управляет временем жизни корутины и позволяет отменить ее выполнение, а также отслеживать ее статус.
- Для чего нужен SupervisorJob?
Используется для управления иерархией корутин, позволяя дочерним корутинам завершаться независимо — сбой одной корутины не отменяет остальных.
- Какие бывают типы Job?
•
Job
•
SupervisorJob
- Для чего нужен Job?
Scope (6)
- Что такое scope в корутинах?
Scope в корутинах управляет областью выполнения корутин, их временем жизни и отменой, обеспечивая контекст для запуска и управления корутинами.
- Какие есть стандартные Scope в Android?
•
lifecycleScope
состоит изDispatchers.Main.immediate
+SupervisorJob()
.•
viewModelScope
состоит изDispatchers.Main.immediate
+SupervisorJob()
.•
MainScope
состоит изDispatchers.Main
+SupervisorJob()
.•
GlobalScope
состоит изEmptyCoroutineContext
.
- Как не отменять корутину если один из ее дочерних элементов вышел из строя?
Использовать
supervisorScope
.
- Разница между coroutineScope и supervisorScope?
coroutineScope
создает новую область видимости корутин, ожидая завершения всех дочерних корутин и отменяя их при ошибке.supervisorScope
, напротив, позволяет дочерним корутинам завершаться независимо, не отменяя остальные при возникновении ошибки, что обеспечивает большую гибкость в обработке ошибок.
- Как дождаться выполнения всех корутин в scope?
Использовать метод
joinAll()
.
- Для чего нужен GlobalScope?
GlobalScope
позволяет запускать корутины, не привязанные к жизненному циклу компонентов, что полезно для фоновых задач. Однако его использование может привести к утечкам памяти, так как корутины продолжают работать после уничтожения компонента.
- Что такое scope в корутинах?
suspend (9)
- Для чего нужно ключевое слово suspend?
Для обозначения функции, которая может быть приостановлена и возобновлена. Оно позволяет выполнять асинхронные операции без блокировки потока, что делает код более читабельным и простым для написания.
- Как работает механизм приостановки?
При вызове
suspend
-функции текущая корутина приостанавливается, позволяя другим корутинам или задачам выполняться в том же потоке. Состояние корутины сохраняется, включая локальные переменные и контекст. Когда асинхронная операция завершена (например, получение данных), корутина возобновляется с того места, где была приостановлена. Это позволяет эффективно управлять асинхронными задачами без блокировки потоков, улучшая отзывчивость приложения.
- Какие ограничения есть у suspend-функций?
Они могут быть вызваны только из другой
suspend
-функции или внутри корутины.
- Во что под капотом разворачивается suspend-функция?
Под капотом
suspend
-функция разворачивается в состояние (state) и объектContinuation
, который сохраняет контекст выполнения. Когда выполнение приостанавливается, состояние функции сохраняется, и управление возвращается в вызывающий код, а при возобновлении выполнение продолжается с сохраненного состояния.
- Где хранятся значения, вычисленные до приостановки корутины?
В объекте
Continuation
.
- Какие данные хранятся в Continuation?
• Состояние корутины, местоположение где была приостановлена.
• Значения всех локальных переменных, вычисленных до приостановки.
• Контекст: Специальные данные, такие как диспетчеры потоков или информация об обработке исключений.
• Ссылка на родительский
Continuation
.
- Что такое стейт-машина корутины?
Continuation
позволяет сохранять текущее состояние выполнения корутины и передавать управление обратно в нее. Этот объект предоставляет методresumeWith
, который используется для возобновления выполнения корутины с определенным результатом или исключением. При компиляции корутины создается стейт-машина, которая управляет состоянием выполнения корутины. Это достигается с помощьюswitch
-case
, которые обрабатывают различные состояния.
- Чем стейт-машина suspend-функции отличается от других стейт-машин?
Тем, что она позволяет приостанавливать выполнение корутины без блокировки потока, возвращая управление при завершении операции. Это делает её легковесной и асинхронной, а код выглядит более линейным и читаемым, как обычные функции.
- Можно ли вызвать suspend-функцию из Java-кода?
Напрямую нет, но можно обернуть ее в
runBlocking
и таким образом вызвать.// Это suspend-функция suspend fun suspendFunction(): String { delay(1000) return "Hello from suspend function!" } // Это функция, которая оборачивает suspend-функцию и делает её доступной для Java-кода fun callSuspendFunction(): String { return runBlocking { suspendFunction() } }
public class Main { public static void main(String[] args) { // Вызов функции из Java-кода String result = KotlinCode.callSuspendFunction(); System.out.println(result); // Выведет: Hello from suspend function! } }
- Для чего нужно ключевое слово suspend?
Синхронизация (5)
- Какие есть способы синхронизировать корутины?
•
Mutex
блокирует доступ к разделяемому ресурсу.•
Channels
обеспечивают безопасную передачу данных между корутинами, предотвращая гонки данных.•
Dispatcher.limitedParallelism(1)
сделать 1 поток.•
Atomic
безопасное обновление значений в многопоточной среде без блокировок.
- Почему synchronized не будет работать в корутинах?
Потому что он блокирует поток, а корутины работают асинхронно и могут сменять потоки во время выполнения.
- Что будет если внутри synchronized вызвать suspend-функцию?
Вызов
suspend
-функции внутриsynchronized
блокирует поток, что нарушает асинхронность корутин и может вызвать дедлоки.
- Какие проблемы могут возникнуть в корутинах при использовании ReentrantLock?
Использование
ReentrantLock
может привести к взаимной блокировке, проблемам с производительностью и усложнению отладки, так как блокировки задерживают выполнение, ожидающее освобождения ресурсов.
- Что такое Mutex?
Объект для синхронизации корутин, который гарантирует, что только одна корутина может получить доступ к ресурсу в один момент времени.
- Какие есть способы синхронизировать корутины?
delay (2)
- Как delay работает под капотом?
Приостанавливает выполнение корутины, освобождая поток на время задержки. При этом диспетчер корутин использует системные таймеры для отслеживания времени, и по его истечении корутина возобновляется. Это не блокирующая операция, и поток может использоваться для других задач.
- Чем Thread.sleep отличается от delay?
Thread.sleep
блокирует текущий поток, тогда какdelay
приостанавливает выполнение корутины, освобождая поток для других задач.
- Как delay работает под капотом?
Context (4)
- Что такое контекст корутины?
Контекст корутины представляет собой набор параметров, которые определяют поведение корутины.
- Разница между Coroutine Context и Coroutine Scope?
CoroutineContext хранит информацию о корутине (диспетчер, job и т.д.), а CoroutineScope объединяет CoroutineContext и управляет жизненным циклом корутин, запущенных в этом контексте.
- Из чего состоит контекст корутины?
•
CoroutineDispatcher
- определяет поток или диспетчер, на котором будет выполняться корутина.•
CoroutineName
- устанавливает имя для отладки.•
CoroutineExceptionHandler
- для обработки исключений.•
Job
- управляет жизненным циклом корутины.•
SupervisorJob
- обеспечить более гибкое управление ошибками и отменой корутин.
- Как переключить контекст выполнения корутины?
Через
withContext
.
- Что такое контекст корутины?
Другие (18)
- Для чего нужны корутины?
Корутины нужны для упрощения работы с асинхронным программированием, позволяя писать код, который выглядит как последовательный, но не блокирует основной поток. Они облегчают управление фоновой работой и обработку задач, таких как сетевые запросы и длительные вычисления, повышая производительность и отзывчивость приложений.
- Основные компоненты корутин?
• CoroutineContext
• CoroutineScope
•
Dispatcher
•
Job
•
Deferred
- Как в корутинах работает структурированный параллелизм (structure cuncurrency)?
Структурированный параллелизм в корутинах гарантирует, что корутины, запущенные в определенной области видимости, завершатся перед выходом из этой области. Это упрощает управление жизненным циклом корутин, предотвращая утечки и ошибки, так как все связанные задачи управляются централизованно и могут быть отменены одновременно.
- Как в корутинах работает backpressure?
В корутинах backpressure управляется через использование каналов и операторов, которые позволяют контролировать поток данных. Когда данные производятся быстрее, чем могут обрабатываться, корутины могут приостановить или замедлить отправку данных в канал, предотвращая переполнение и обеспечивая более плавное и безопасное выполнение.
- Различие между RxJava и Coroutines?
• Модель работы: RxJava использует реактивное программирование, корутины — асинхронный код в последовательном стиле.
• Использование ресурсов: Корутины более легковесные и эффективные при высокой нагрузке.
- Чем корутины отличаются от обычных потоков?
Отличаются тем, что они легковесны, не блокируют потоки при переключении контекста, позволяют писать асинхронный код в синхронном стиле и интегрируются с механизмами управления жизненным циклом для более эффективного управления задачами.
- Что занимает больше ресурсов: поток или корутина?
Корутина занимает меньше ресурсов, чем поток, поскольку она легче по весу, не требует отдельного стека и управляется внутри одного потока, что позволяет запускать тысячи корутин в рамках одного потока. Потоки же создают большую нагрузку на систему, используя больше памяти и процессорного времени для управления.
- Почему корутины называют легковесными потоками?
Корутины называют легковесными потоками, потому что они используют меньше ресурсов, чем традиционные потоки. Они могут создаться и управляться в одном потоке, позволяя запускать тысячи корутин без значительных затрат на память и производительность.
- Как в корутинах отправить Single Event?
Использовать
Channel
илиSharedFlow
с настройкамиreplay
в 0.
- Может ли корутина продолжить свое выполнение на другом потоке?
Да, это достигается с помощью контекстов корутин, которые позволяют переключать контексты и тем самым изменять поток выполнения.
- За счет чего в корутинах можно писать параллельный код? кратко
Параллельный код в корутинах возможен благодаря асинхронным конструкциям (
async
,launch
) и диспетчерам, которые распределяют задачи по разным потокам, обеспечивая независимое выполнение блоков кода.
- Можно ли внутри корутины запустить этот код?
try { callSomeSuspendFunction() } catch (e: Exception) { ... }
Да, в корутине можно использовать
try-catch
для обработки исключений, возникающих при вызовеsuspend
функций.
- Будет ли такой код работать?
class MainActivity: AppCompatActivity() { val scope = CoroutineScope(Dispatchers.Main) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) scope.launch { repeat(1000) { delay(1000) } } } }
Да, но есть проблема: корутина, запущенная в scope, продолжит выполняться даже после уничтожения
MainActivity
, что приведет к утечке памяти. Чтобы этого избежать, нужно связатьCoroutineScope
с жизненным цикломActivity
. Для этого лучше использоватьlifecycleScope
, который автоматически отменяет корутины при уничтоженииActivity
.
- Какой результат выведется в консоль?
suspend fun main() { val scope = CoroutineScope(Job()) scope.launch { delay(2000) error("") } scope.launch { repeat(5) { Thread.sleep(999) println("Number: $it, isActive = $isActive") } }.join() }
Через 2 секунды первая корутина завершится с исключением, что приведёт к отмене второй корутины.
- Что будет выведено в println?
fun main(): Unit = runBlocking { val exceptionHandler = CoroutineExceptionHandler { _, _ -> print("⚠️") } val scope = CoroutineScope(context = SupervisorJob() + exceptionHandler) val job1 = scope.launch { print("A") delay(timeMillis = 500) print("B") throw Exception() } val job2 = scope.launch { print("C") delay(timeMillis = 1000) print("D") } joinAll(job1, job2) }
AC⚠️BD
- Что будет выведено в println?
val job = launch { try { repeat(times = 5) { i -> print("A$i") delay(timeMillis = 100) } } finally { print("B") } } delay(timeMillis = 250) print("C") job.cancelAndJoin() print("D")
A0A1CBD
. Корутина выводит «A0», «A1», затем «C», отменяется, выводит «B» вfinally
, и завершается с «D».
- Какой метод используется для запуска coroutine в пределах существующей coroutine, ожидая ее завершения?
•
withContext
•
async
•
await
•
launch
- Какой метод используется для выполнения coroutine в главном потоке UI в Android?
•
withMainThread
•
withContext(Dispatchers.Main)
•
withUl
•
runOnUl
- Для чего нужны корутины?