Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,395 @@
|
||||
---
|
||||
title: Suspend-функции
|
||||
description: Модификатор suspend, Continuation, машина состояний и мост к колбэчным API
|
||||
tags:
|
||||
- kotlin
|
||||
- coroutines
|
||||
---
|
||||
|
||||
`suspend` означает: эта функция может приостановиться, поэтому вызывать её разрешено только оттуда, где приостановка возможна.
|
||||
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun fetchData(): String {
|
||||
delay(100) // точка приостановки
|
||||
return "данные"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(fetchData()) // данные
|
||||
}
|
||||
```
|
||||
Вне корутины вызов не скомпилируется: `fun main() { fetchData() }` даёт «Suspend function can only be called within coroutine body».
|
||||
|
||||
## Что добавляет компилятор
|
||||
`suspend fun fetchData(): String` превращается в метод с дополнительным параметром:
|
||||
```kotlin
|
||||
// примерно так это выглядит с точки зрения Java:
|
||||
// Object fetchData(Continuation<? super String> continuation)
|
||||
```
|
||||
`Continuation` - это «что делать дальше»: объект с методом `resumeWith`, хранящий сохранённое состояние функции.
|
||||
```kotlin
|
||||
import kotlin.coroutines.*
|
||||
|
||||
fun main() {
|
||||
val continuation = object : Continuation<String> {
|
||||
override val context = EmptyCoroutineContext
|
||||
override fun resumeWith(result: Result<String>) {
|
||||
println("получено: ${result.getOrNull()}")
|
||||
}
|
||||
}
|
||||
continuation.resumeWith(Result.success("значение")) // получено: значение
|
||||
}
|
||||
```
|
||||
Отсюда и название CPS - continuation-passing style. Компилятор делает это преобразование автоматически, поэтому в исходнике остаётся обычный последовательный код.
|
||||
|
||||
## Машина состояний
|
||||
Каждая точка приостановки превращается в метку состояния:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun threeSteps(): Int {
|
||||
println("шаг 1") // состояние 0
|
||||
delay(10)
|
||||
println("шаг 2") // состояние 1
|
||||
delay(10)
|
||||
println("шаг 3") // состояние 2
|
||||
return 42
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(threeSteps())
|
||||
// шаг 1
|
||||
// шаг 2
|
||||
// шаг 3
|
||||
// 42
|
||||
}
|
||||
```
|
||||
В байт-коде это один метод с `switch` по полю `label`: при каждом возобновлении он входит в себя заново и прыгает к нужной метке. Локальные переменные, живущие между приостановками, становятся полями объекта-продолжения.
|
||||
|
||||
Практические следствия:
|
||||
- приостановка стоит дешевле переключения потока, но дороже обычного вызова
|
||||
- глубоко вложенные `suspend`-вызовы создают по объекту продолжения на кадр
|
||||
- стектрейс восстанавливается библиотекой, а не JVM - поэтому он может быть короче ожидаемого
|
||||
|
||||
## Функция, которая не приостанавливается
|
||||
`suspend` не обязывает приостанавливаться:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun maybeSlow(cached: String?): String {
|
||||
cached?.let { return it } // быстрый путь: без приостановки
|
||||
delay(100)
|
||||
return "загружено"
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(maybeSlow("из кеша")) // из кеша
|
||||
println(maybeSlow(null)) // загружено
|
||||
}
|
||||
```
|
||||
Если приостановки не произошло, функция возвращает значение напрямую - без накладных расходов. Это важно для «горячих» путей: `suspend` не означает «всегда медленно».
|
||||
|
||||
Компилятор предупреждает о `suspend` без точек приостановки:
|
||||
```kotlin
|
||||
suspend fun noSuspension(): Int = 42 // warning: redundant 'suspend' modifier
|
||||
|
||||
fun main() {
|
||||
println("нужен вызов из корутины или runBlocking")
|
||||
}
|
||||
```
|
||||
Такой модификатор стоит убрать - он заставляет вызывающих быть `suspend` без причины.
|
||||
|
||||
## suspend-лямбды и типы
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
suspend fun retry(times: Int, block: suspend () -> String): String {
|
||||
repeat(times - 1) { attempt ->
|
||||
try {
|
||||
return block()
|
||||
} catch (e: Exception) {
|
||||
println("попытка ${attempt + 1}: ${e.message}")
|
||||
delay(10)
|
||||
}
|
||||
}
|
||||
return block()
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
var counter = 0
|
||||
val result = retry(3) {
|
||||
counter++
|
||||
delay(5)
|
||||
if (counter < 3) error("не готово") else "успех"
|
||||
}
|
||||
println(result)
|
||||
// попытка 1: не готово
|
||||
// попытка 2: не готово
|
||||
// успех
|
||||
}
|
||||
```
|
||||
Тип `suspend () -> String` - функциональный тип, внутри которого можно приостанавливаться. Без `suspend` в типе параметра `delay` внутри лямбды не скомпилируется.
|
||||
|
||||
Для лямбды с получателем аналогично: `suspend CoroutineScope.() -> Unit` - именно так объявлены `launch` и `runBlocking`.
|
||||
|
||||
## Приостановка без блокировки: suspendCancellableCoroutine
|
||||
Это мост от колбэчного API к `suspend`-функции - основной способ интегрировать существующие библиотеки.
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
// колбэчный API, который нужно обернуть
|
||||
fun legacyLoad(onSuccess: (String) -> Unit, onError: (Throwable) -> Unit) {
|
||||
Thread {
|
||||
Thread.sleep(50)
|
||||
onSuccess("результат из колбэка")
|
||||
}.start()
|
||||
}
|
||||
|
||||
suspend fun load(): String = suspendCancellableCoroutine { cont ->
|
||||
legacyLoad(
|
||||
onSuccess = { cont.resume(it) },
|
||||
onError = { cont.resumeWithException(it) },
|
||||
)
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(load()) // результат из колбэка
|
||||
}
|
||||
```
|
||||
Правила: `resume` или `resumeWithException` нужно вызвать **ровно один раз**. Повторный вызов бросит `IllegalStateException`.
|
||||
|
||||
Отмену нужно поддержать явно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
suspend fun awaitFuture(future: CompletableFuture<String>): String =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
future.whenComplete { value, error ->
|
||||
if (error != null) cont.cancel(error) else cont.resume(value)
|
||||
}
|
||||
cont.invokeOnCancellation { future.cancel(true) } // освобождаем ресурс
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
val future = CompletableFuture.supplyAsync {
|
||||
Thread.sleep(30)
|
||||
"из future"
|
||||
}
|
||||
println(awaitFuture(future)) // из future
|
||||
}
|
||||
```
|
||||
`invokeOnCancellation` - то, что отличает корректную обёртку от утечки: при отмене корутины нижележащий запрос тоже нужно отменить. Подробнее: [Отмена и таймауты](Отмена%20и%20таймауты.md)
|
||||
|
||||
## Последовательность и параллельность
|
||||
`suspend`-функции по умолчанию выполняются последовательно:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
suspend fun step(name: String): String { delay(100); return name }
|
||||
|
||||
fun main() = runBlocking {
|
||||
val sequential = measureTimeMillis {
|
||||
val a = step("a")
|
||||
val b = step("b")
|
||||
println("$a$b") // ab
|
||||
}
|
||||
println("последовательно около 200 мс: ${sequential >= 200}") // последовательно около 200 мс: true
|
||||
|
||||
val parallel = measureTimeMillis {
|
||||
coroutineScope {
|
||||
val a = async { step("a") }
|
||||
val b = async { step("b") }
|
||||
println("${a.await()}${b.await()}") // ab
|
||||
}
|
||||
}
|
||||
println("параллельно около 100 мс: ${parallel < 180}") // параллельно около 100 мс: true
|
||||
}
|
||||
```
|
||||
Это сознательное решение: последовательность - разумное поведение по умолчанию, параллельность требуется явно запросить через `async`. Подробнее: [Билдеры корутин](Билдеры%20корутин.md)
|
||||
|
||||
## Правила проектирования suspend-API
|
||||
| Правило | Почему |
|
||||
|---|---|
|
||||
| `suspend fun` не должна блокировать поток | иначе теряется весь смысл |
|
||||
| смена диспетчера - внутри функции, а не у вызывающего | вызывающий не должен знать, I/O это или CPU |
|
||||
| функция должна быть безопасна для отмены | иначе отмена не работает или течёт |
|
||||
| возвращать значение, а не `Deferred` | параллельность - дело вызывающего |
|
||||
| `suspend` + возврат `Flow` обычно излишни | `Flow` сам по себе холодный |
|
||||
|
||||
Правило про диспетчер - самое практичное:
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
// хорошо: вызывающему не нужно думать о потоках
|
||||
suspend fun readConfig(): String = withContext(Dispatchers.IO) {
|
||||
"содержимое файла" // здесь был бы блокирующий File.readText
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(readConfig()) // содержимое файла
|
||||
}
|
||||
```
|
||||
Такая функция называется main-safe: её можно вызвать с любого потока, включая UI.
|
||||
|
||||
## suspend и наследование
|
||||
```kotlin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
interface Repository {
|
||||
suspend fun find(id: Int): String?
|
||||
}
|
||||
|
||||
class RemoteRepository : Repository {
|
||||
override suspend fun find(id: Int): String? {
|
||||
delay(10)
|
||||
return if (id == 1) "найдено" else null
|
||||
}
|
||||
}
|
||||
|
||||
class CachedRepository : Repository {
|
||||
private val cache = mapOf(1 to "из кеша")
|
||||
override suspend fun find(id: Int): String? = cache[id] // без приостановки
|
||||
}
|
||||
|
||||
fun main() = runBlocking {
|
||||
println(RemoteRepository().find(1)) // найдено
|
||||
println(CachedRepository().find(1)) // из кеша
|
||||
println(CachedRepository().find(2)) // null
|
||||
}
|
||||
```
|
||||
`suspend` в интерфейсе - правильный способ объявить операцию, которая **может** быть асинхронной. Реализация решает, приостанавливаться ли. Обратное невозможно: не-suspend метод интерфейса нельзя переопределить как `suspend`.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `CompletableFuture<T> f()` | `suspend fun f(): T` |
|
||||
> | `void f(Callback<T> cb)` | `suspend fun f(): T` |
|
||||
> | `f().thenApply(x -> ...)` | `val x = f()` |
|
||||
> | `CompletionStage` в сигнатуре | `suspend` в модификаторе |
|
||||
> | блокирующий `f()` + пул потоков | `suspend` + `withContext(IO)` |
|
||||
> | нет аналога | `Continuation` |
|
||||
>
|
||||
> **Асинхронность в типе возврата против модификатора.** В Java асинхронная функция возвращает обёртку: `CompletableFuture<User>`. Это заражает тип: `List<CompletableFuture<User>>` приходится разворачивать через `allOf`, а вложенные вызовы дают `CompletableFuture<CompletableFuture<T>>` и `thenCompose` вместо `thenApply`. В Kotlin тип возврата остаётся обычным (`User`), а асинхронность выражается модификатором:
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun loadOne(id: Int): String { delay(10); return "u$id" }
|
||||
>
|
||||
> suspend fun loadMany(ids: List<Int>): List<String> = coroutineScope {
|
||||
> ids.map { async { loadOne(it) } }.awaitAll()
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(loadMany(listOf(1, 2, 3))) } // [u1, u2, u3]
|
||||
> ```
|
||||
> В Java это `CompletableFuture.allOf(...)` с ручной сборкой результатов и приведением типов.
|
||||
>
|
||||
> **`Continuation` - это обобщённый коллбэк.** В сущности `suspend fun f(): T` компилируется в `f(Continuation<T>)`, то есть в тот же колбэчный стиль, который в Java пишут руками. Разница в том, что преобразование делает компилятор, поэтому исходник остаётся линейным, а в Java программист пишет лямбды сам и получает «callback hell» при вложенности.
|
||||
>
|
||||
> **Из Java `suspend`-функцию вызвать неудобно.** Она видна как метод с лишним параметром `Continuation`, возвращающий `Object`. Для интеграции есть готовые мосты: `future { }` из `kotlinx-coroutines-jdk8` возвращает `CompletableFuture`, а `runBlocking` даёт синхронный вызов. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
|
||||
>
|
||||
> **Виртуальные потоки убирают необходимость в модификаторе.** В Java 21 блокирующий `loadOne(id)` на виртуальном потоке масштабируется без изменения сигнатуры - это главный плюс Loom: существующий код работает как есть. Цена - компилятор не знает, где функция блокируется, поэтому нет ни структурированной отмены по дереву, ни проверки «не блокируй UI-поток» на этапе компиляции.
|
||||
>
|
||||
> Сравнить: [Многопоточность в Java](../../Программирование%20на%20Java/Многопоточность/Взаимодейтсвие%20между%20потоками.md), [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Блокировка внутри `suspend` сводит корутины на нет:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> // плохо: функция объявлена suspend, но блокирует поток
|
||||
> suspend fun badRead(): String {
|
||||
> Thread.sleep(50)
|
||||
> return "заблокировал поток"
|
||||
> }
|
||||
>
|
||||
> // хорошо: блокирующий код в подходящем диспетчере
|
||||
> suspend fun goodRead(): String = withContext(Dispatchers.IO) {
|
||||
> Thread.sleep(50)
|
||||
> "поток из IO-пула"
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> println(badRead())
|
||||
> println(goodRead())
|
||||
> // заблокировал поток
|
||||
> // поток из IO-пула
|
||||
> }
|
||||
> ```
|
||||
> Компилятор это не поймает: `suspend` не запрещает блокировку. Ошибка проявится как «приложение зависает под нагрузкой».
|
||||
>
|
||||
> **Повторный `resume` бросает исключение:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
> import kotlin.coroutines.resume
|
||||
>
|
||||
> suspend fun broken(): String = suspendCancellableCoroutine { cont ->
|
||||
> cont.resume("первый")
|
||||
> // cont.resume("второй") // IllegalStateException: Already resumed
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(broken()) } // первый
|
||||
> ```
|
||||
> Колбэчные API, которые могут вызвать коллбэк дважды (например, при ретраях внутри), требуют защиты флагом или `isActive`-проверки.
|
||||
>
|
||||
> **Обёртка без `invokeOnCancellation` течёт.** При отмене корутины нижележащий запрос продолжит работу: соединение останется открытым, таймер - запущенным, файл - читаемым. Всегда освобождайте ресурс в `invokeOnCancellation`.
|
||||
>
|
||||
> **`suspendCoroutine` без `Cancellable` игнорирует отмену:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> val job = launch {
|
||||
> try {
|
||||
> withTimeout(30) { delay(1000) }
|
||||
> } catch (e: TimeoutCancellationException) {
|
||||
> println("таймаут сработал: ${e::class.simpleName}")
|
||||
> }
|
||||
> }
|
||||
> job.join()
|
||||
> }
|
||||
> ```
|
||||
> `delay` отменяем, поэтому таймаут работает. А вот `suspendCoroutine { }` (без `Cancellable`) приостановится навсегда и таймаут его не прервёт - используйте `suspendCancellableCoroutine`.
|
||||
>
|
||||
> **`suspend` поднимается по всему стеку вызовов.** Добавить приостановку в глубоко вложенную функцию означает помечать `suspend` всю цепочку вверх. Если наверху - не-suspend интерфейс (например, `Comparator` или `equals`), сделать это невозможно, и придётся либо менять архитектуру, либо блокировать через `runBlocking`.
|
||||
>
|
||||
> **`suspend` нельзя использовать в конструкторе, геттере свойства и `init`:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> class Service {
|
||||
> // suspend init — невозможно
|
||||
> // val value: String get() = loadValue() // геттер не может быть suspend
|
||||
>
|
||||
> suspend fun loadValue(): String { delay(10); return "значение" }
|
||||
> }
|
||||
>
|
||||
> fun main() = runBlocking { println(Service().loadValue()) } // значение
|
||||
> ```
|
||||
> Для асинхронной инициализации используют отдельную `suspend fun init()` или `lazy`-обёртку через `Deferred`.
|
||||
>
|
||||
> **Нельзя переопределить обычный метод как `suspend`.** Если интерфейс объявлен в Java (`Runnable`, `Callable`, `Comparator`), асинхронность туда не поместится - только `runBlocking` внутри, что блокирует поток.
|
||||
>
|
||||
> **Стектрейсы обрезаны.** Исключение из корутины содержит кадры только текущей точки возобновления, а не всю логическую цепочку. Для отладки включают `-Dkotlinx.coroutines.debug` или добавляют `kotlinx-coroutines-debug`, которые восстанавливают историю приостановок.
|
||||
>
|
||||
> **Рекурсивная `suspend`-функция создаёт объект продолжения на каждый кадр:**
|
||||
> ```kotlin
|
||||
> import kotlinx.coroutines.*
|
||||
>
|
||||
> suspend fun countDown(n: Int): Int = if (n <= 0) 0 else countDown(n - 1) + 1
|
||||
>
|
||||
> fun main() = runBlocking {
|
||||
> println(countDown(1000)) // 1000
|
||||
> }
|
||||
> ```
|
||||
> Глубина ограничена не стеком JVM, а памятью, но на десятках тысяч кадров это заметная аллокация. Хвостовая рекурсия (`tailrec`) с `suspend` не сочетается.
|
||||
>
|
||||
> **`suspend` без приостановок - лишний.** Он заставляет всех вызывающих быть `suspend`, не давая ничего взамен. Компилятор предупреждает; предупреждение стоит слушать.
|
||||
|
||||
Далее: [Структурированная конкурентность](Структурированная%20конкурентность.md)
|
||||
Reference in New Issue
Block a user