Suspend function — це функція з модифікатором suspend, яка може призупиняти своє виконання без блокування потоку та відновлюватися пізніше в тій самій корутині. За даними JetBrains Kotlin Docs, 2025, suspend-функції є фундаментальним будівельним блоком корутин, що забезпечує асинхронність без callback-ів. Кожна suspend-функція компілюється в state-машину на базі Continuation, що дозволяє ефективно керувати точками призупинення.
Головне
Suspend function — це функція, оголошена з ключовим словом suspend, яка може призупиняти виконання в одній або кількох точках, не блокуючи потік. Кожен виклик suspend-функції всередині іншої suspend-функції є потенційною точкою призупинення.
suspend fun fetchUserData(): User {
val response = httpClient.get("/user")
return parser.parse(response)
}
Компілятор Kotlin транслює таку функцію в state-машину. Кожна точка призупинення (виклик іншої suspend-функції) стає станом (label). Поточний потік звільняється між станами, а після завершення очікуваної операції виконання відновлюється з наступного стану.
Suspend-функції з'явилися в Kotlin 1.3 (2018 рік) разом із корутинами як експериментальна фіча та стали стабільними в Kotlin 1.5 (2021 рік). До цього асинхронність у Kotlin/Java забезпечувалася через callback-и, RxJava та CompletableFuture. Suspend-функції запропонували альтернативу з лінійним синтаксисом та автоматичним управлінням потоками.
Розуміння внутрішнього устрою suspend-функцій — ключ до правильної роботи з корутинами. На відміну від звичайних функцій, кожна suspend-функція компілюється в клас з інтерфейсом Continuation.
Компілятор Kotlin додає параметр типу Continuation в кінець кожного параметра suspend-функції. Continuation містить:
Припустимо, у нас є suspend-функція з двома викликами інших suspend-функцій:
suspend fun process() {
val a = stepOne()
val b = stepTwo(a)
println(b)
}
Компілятор перетворює її в state-машину з мітками:
// Simplified generated code representation
fun process(cont: Continuation<Unit>): Any? {
val cont = cont as ProcessContinuation
when (cont.label) {
0 -> {
cont.label = 1
if (stepOne(cont) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
1 -> {
cont.label = 2
val a = cont.result as TypeA
if (stepTwo(a, cont) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
2 -> {
println(cont.result)
Unit
}
}
}
Ключове спостереження: якщо функція повертає COROUTINE_SUSPENDED, поточний потік звільняється. Коли асинхронна операція завершується, Continuation.resumeWith викликається, і state-машина продовжує з наступного label.
Оголошення suspend-функції нічим не відрізняється від звичайної, крім ключового слова suspend перед fun. Обмеження тільки одне: викликати suspend-функцію можна тільки з корутини або іншої suspend-функції.
suspend fun delayAndReturn(ms: Long): String {
delay(ms)
return "Done after ${ms}ms"
}
У цьому прикладі delay — теж suspend-функція, яка призупиняє корутину на вказану кількість мілісекунд, не блокуючи потік. Після затримки виконання відновлюється.
fun main() = runBlocking {
val result = delayAndReturn(1000)
println(result)
}
runBlocking створює міст між звичайним світом і корутинами. Усередині лямбди доступний виклик будь-яких suspend-функцій.
Kotlin підтримує suspend-версії функціональних типів — suspend () -> T та suspend (A) -> B. Це дозволяє передавати асинхронні лямбди в функції вищого порядку:
suspend fun withRetry (
retries: Int = 3,
block: suspend () -> T
): T {
repeat(retries - 1) {
try { return block() }
catch (_: Exception) { delay(100) }
}
return block()
}
Функція withRetry приймає suspend-лямбду та повторює її виконання при помилках. Це типовий патерн для мережевих запитів з ретраями.
Відмінності між suspend і звичайними функціями виходять за рамки простого додавання модифікатора. Розглянемо принципові відмінності.
| Характеристика | Звичайна функція | Suspend-функція |
|---|---|---|
| Потік виконання | Блокує потік до завершення | Може звільнити потік і відновитися пізніше |
| Параметри компілятора | Тільки вказані параметри | Неявний Continuation в кінці |
| Виклик зі звичайної функції | Так | Ні |
| Стек | Фізичний стек потоку | State-машина в heap + фізичний стек між точками |
| Повернення | Безпосереднє значення | Значення або COROUTINE_SUSPENDED |
| Продуктивність | Мінімальні накладні витрати | ~кілька наносекунд на state-машину (Kotlin 1.9+) |
Звичайна функція не має Continuation — їй нікуди зберегти стан і нічим відновити виконання. Якщо потрібно викликати suspend-функцію зі звичайної, використовуйте runBlocking (для тестів) або CoroutineScope.launch (для production з урахуванням lifecycle).
Розглянемо три реальних сценарії використання suspend-функцій в Android-додатках на Kotlin.
Room підтримує suspend-функції безпосередньо — запит виконується у фоновому потоці автоматично:
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE id = :id")
suspend fun getUser(id: Int): User?
@Insert
suspend fun insertUser(user: User)
}
Room внутрішньо використовує Dispatchers.IO для виконання запиту, а результат повертається в той диспетчер, на якому була викликана suspend-функція.
class ProfileViewModel : ViewModel() {
private val repo = ProfileRepository()
fun loadProfile(id: String) {
viewModelScope.launch {
val profile = repo.getProfile(id)
_profile.value = profile
}
}
}
ViewModelScope.launch створює корутину, всередині якої викликається suspend-функція getProfile. Після отримання результату UI оновлюється на головному потоці.
suspend fun placeOrder(cart: Cart): OrderResult {
val validated = validateCart(cart)
val payment = processPayment(validated)
val receipt = sendReceipt(payment)
return receipt
}
Три suspend-функції виконуються послідовно. На кожному кроці корутина може призупинитися, не блокуючи потік. Якщо якийсь крок викине виняток — інші не виконаються, що захищає від некоректних станів замовлення.
Навіть досвідчені розробники Kotlin допускають помилки при проектуванні suspend-функцій. Розглянемо найчастіші.
Suspend-функція не робить код автоматично асинхронним. Thread.sleep(), InputStream.read() та інші блокуючі виклики все одно заблокують потік. Використовуйте withContext(Dispatchers.IO) для обгортання блокуючих операцій.
Якщо функція не викликає інших suspend-функцій і не виконує асинхронних операцій — модифікатор suspend надлишковий. Він додає накладні витрати на state-машину та обмежує контекст виклику. Робіть функцію suspend тільки коли вона дійсно призупиняється.
При скасуванні корутини suspend-функції викидають CancellationException. Не ловіть його бездумно — ви позбавляєте код, що викликає, можливості коректно завершити скасування. Якщо потрібно виконати фіналізуючу операцію, використовуйте finally блок і NonCancellable.
suspend fun safeOperation() {
try {
doWork()
} finally {
withContext(NonCancellable) {
cleanup()
}
}
}
Блок finally виконується завжди, включаючи скасування, а NonCancellable гарантує, що cleanup не перерветься.
Не можна викликати suspend-функцію безпосередньо з callback без створення корутини. Використовуйте suspendCoroutine або suspendCancellableCoroutine для адаптації callback-стилю до корутин.
Часто задавані питання
Так, технічно suspend-функція може не викликати інших suspend-функцій. Компілятор створить state-машину з одним станом (label 0). Однак практичної користі в такій функції немає — вона виконується як звичайна, але з накладними витратами. Не використовуйте suspend без необхідності.
Kotlin надає kotlinx-coroutines-debug — бібліотеку з DebugProbes та інструментами трасування корутин. В Android Studio починаючи з версії Arctic Fox є вбудована вкладка Coroutines у Debugger, яка показує активні корутини, їх стан і точки призупинення.
Кожна точка призупинення створює новий стан у state-машині. Для більшості додатків накладні витрати однієї точки становлять одиниці наносекунд (Kotlin 1.9+). Тільки при десятках тисяч точок у циклі варто об'єднувати операції або використовувати sequence/flow.
У Kotlin suspend — модифікатор типу функції, а не маркер значення, що повертається (як async у C#). Будь-яка suspend-функція може мати будь-які параметри та тип, що повертається, і її виклик синтаксично не відрізняється від виклику звичайної функції — немає await-оператора на місці виклику.
Використовуйте suspendCancellableCoroutine для адаптації. Всередині ви передаєте реєстрацію callback, який викликає continuation.resume(), і повертаєте токен скасування, якщо callback підтримує відписку. Це стандартний патерн для обгортки старих Android API.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також