--- 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 continuation) ``` `Continuation` - это «что делать дальше»: объект с методом `resumeWith`, хранящий сохранённое состояние функции. ```kotlin import kotlin.coroutines.* fun main() { val continuation = object : Continuation { override val context = EmptyCoroutineContext override fun resumeWith(result: Result) { 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 = 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 f()` | `suspend fun f(): T` | > | `void f(Callback cb)` | `suspend fun f(): T` | > | `f().thenApply(x -> ...)` | `val x = f()` | > | `CompletionStage` в сигнатуре | `suspend` в модификаторе | > | блокирующий `f()` + пул потоков | `suspend` + `withContext(IO)` | > | нет аналога | `Continuation` | > > **Асинхронность в типе возврата против модификатора.** В Java асинхронная функция возвращает обёртку: `CompletableFuture`. Это заражает тип: `List>` приходится разворачивать через `allOf`, а вложенные вызовы дают `CompletableFuture>` и `thenCompose` вместо `thenApply`. В Kotlin тип возврата остаётся обычным (`User`), а асинхронность выражается модификатором: > ```kotlin > import kotlinx.coroutines.* > > suspend fun loadOne(id: Int): String { delay(10); return "u$id" } > > suspend fun loadMany(ids: List): List = 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)`, то есть в тот же колбэчный стиль, который в 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)