1. suspend і delay: очікування без блокування
Коли ви лише починаєте програмувати, очікування здається найпростішою дією у світі: «ну поставлю паузу на 200 мс — і все». Але компʼютер у цей момент не медитує — він утримує ресурси. Якщо очікування зроблено блокувальним способом, ви займаєте потік і заважаєте програмі робити щось іще (іноді — навіть показувати інтерфейс або обробляти введення).
Уявіть, що потік — це офіціант у кафе. Якщо офіціант відійшов і став біля стіни чекати рівно 5 хвилин, поки ви доїсте суп, то він не приймає нові замовлення й не несе страви іншим столикам. Оце і є блокування: потік зайнятий очікуванням і не приносить користі.
Корутини в Kotlin якраз і створили для того, щоб очікування можна було оформити інакше. Корутина «стає на паузу», а потік у цей момент може зайнятися іншою роботою. В офіційних матеріалах Kotlin це описують як можливість призупиняти виконання без блокування системних ресурсів.
suspend: чесна позначка «я можу чекати»
Слово suspend часто лякає новачків, бо виглядає як «чарівний режим». Насправді це майже як наліпка на коробці: «обережно, усередині може бути очікування». suspend не обіцяє пришвидшення, не обіцяє окремий потік і не обіцяє паралельність. Він гарантує лише одне: цю функцію дозволено призупиняти (тобто всередині можуть бути точки призупинення).
Звідси одразу випливає практичне правило. Якщо ваша функція всередині робить «правильне очікування» через корутинні механізми, то вона майже напевно має бути suspend. І тоді компілятор починає вас захищати: він не дозволить викликати таку функцію зі звичайного коду «як завгодно».
Подивімося на мінімальний приклад «функції з чесним контрактом»:
import kotlinx.coroutines.delay
suspend fun loadGreeting(): String {
delay(200)
return "Привіт!"
}
Тут важливий не рядок "Привіт!", а те, що delay(200) вимагає корутинного контексту. А отже, і функція зобовʼязана бути suspend.
Якщо спробувати викликати її напряму зі звичайного main, компілятор не дасть:
fun main() {
val text = loadGreeting() // Помилка компіляції: suspend function...
println(text)
}
Це не «шкідливість Kotlin», а захист від непередбачуваного очікування в невідповідному місці.
delay(...): «пауза» без блокування потоку
Тепер перейдемо до героя лекції — delay. На рівні відчуттів delay(200) схожий на Thread.sleep(200): «почекай 200 мілісекунд». Але сенс у них різний. delay — це suspend-функція з kotlinx.coroutines, яка призупиняє корутину, а не блокує потік. Саме тому її можна викликати лише всередині корутинного коду.
Найпростіший каркас (який у нас уже був у лекції про runBlocking) виглядає так:
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
println("A") // A
delay(200)
println("B") // B
}
Ключовий момент для новачка: так, візуально програма «стоїть» між "A" і "B". Але очікування оформлене так, що корутина може поступитися виконанням, а не тримати потік «у заручниках».
Точка призупинення: де корутина має право «стати на паузу»
delay(...) — класичний приклад точки призупинення. Це місце, де корутина може зупинити своє виконання й продовжити пізніше. У документації Kotlin корутини описують саме так: код можна призупиняти й відновлювати в природному послідовному стилі через suspending-функції.
Невеличка схема для інтуїції:
flowchart TD
A["Корутина виконує код"] --> B["Зустрічає delay(...)"]
B --> C["Корутина призупинена"]
C --> D["Минає час"]
D --> E["Корутина продовжує виконання після delay"]
Важливо: це схема про корутину, а не про потік. Потік — окрема сутність, і він може бути вільним робити інше, поки корутина «спить».
2. Thread.sleep(...): пауза, яка реально «заморожує» потік
Тепер порівняймо з класикою Java-світу — Thread.sleep. Ця функція робить рівно те, що написано: присипляє поточний потік. Тобто офіціант справді йде в підсобку й зачиняє двері на ключ.
І ось тут зʼявляється типова пастка: «Але ж я всередині runBlocking, отже це корутини, отже Thread.sleep ніби “ок”». Ні. Корутини не скасовують фізики. Якщо ви викликаєте Thread.sleep, потік блокується по-справжньому.
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
println("Перед sleep") // Перед sleep
Thread.sleep(200)
println("Після sleep") // Після sleep
}
Ззовні поведінка схожа на delay, але семантично — зовсім інша. delay — це «корутина почекає», Thread.sleep — це «потік почекає».
Щоб зафіксувати різницю, стане в пригоді маленька таблиця:
| Що робимо | Чим чекаємо | Що «стає на паузу» | Потрібен suspend | Чи можна викликати зі звичайної функції |
|---|---|---|---|---|
| Пауза в корутинному коді | |
Корутина | Так | Ні |
| Пауза будь-де | |
Потік | Ні | Так |
3. Практичний патерн: очікування всередині suspend-функції
У цьому розділі ми зробимо найкорисніший крок. Ми не просто подивимося на delay, а вбудуємо його в стиль програмування, який не розвалиться за тиждень. У курсах часто показують delay(100) прямо в main, але в реальному проєкті так зазвичай не роблять. Хороший стиль — оформлювати «довгі операції» окремими функціями й робити їхній контракт чесним.
Припустімо, у нас є консольний мініпроєкт (ми його розвивали нещодавно в темі про колекції та команди): простий облік витрат, де є команди add, list, total. Тепер ми хочемо додати команду sync, яка «ніби» синхронізує дані із сервером. Сервер у нас поки уявний, тож затримку будемо симулювати.
Зробімо suspend-функцію:
import kotlinx.coroutines.delay
suspend fun syncExpenses(): String {
delay(500)
return "Синхронізацію завершено"
}
А тепер використаємо її в корутинному сценарії через runBlocking:
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val result = syncExpenses()
println(result) // Синхронізацію завершено
}
Це важлива архітектурна звичка: ви не «розмазуєте» очікування по всьому коду, а зосереджуєте його там, де воно логічно належить до операції.
Для порівняння: «блокувальна версія» тієї ж операції
Іноді корисно зробити аналог — так мозок краще помічає різницю:
fun syncExpensesBlocking(): String {
Thread.sleep(500)
return "Синхронізацію завершено (blocking)"
}
І виклик:
fun main() {
val result = syncExpensesBlocking()
println(result) // Синхронізацію завершено (blocking)
}
Ззовні схоже. Але перша версія вбудовується в корутинний світ і «дружить» з іншими suspend-операціями, а друга — завжди про блокування потоку.
4. Чому delay не «прискорює», і як налагоджувати очікування
Чому delay не «прискорює програму», але все одно правильніший
Цей розділ зазвичай викликає внутрішню суперечку: «Якщо в мене в консольній програмі один main і я просто чекаю 500 мс, то яка різниця — delay чи Thread.sleep? Усе одно ж 500 мс мине». І це хороше запитання: у мінімальному прикладі справді здається, що різниці немає. На рівні часу «між A і B» — так, однаково.
Різниця проявляється, коли програма стає складнішою і в ній зʼявляється більше роботи, ніж одна лінійна послідовність. Тоді блокування потоку починає заважати: потік зайнятий очікуванням і не може обслуговувати інші завдання. Корутини ж, навпаки, проєктують як легкі одиниці виконання. Їх можна призупиняти без блокування системних ресурсів.
Важливо сформулювати думку акуратно, не забігаючи наперед: delay не робить магії сам по собі — він просто робить очікування сумісним із корутинною моделлю. Навіть якщо просто зараз ви не запускаєте кілька корутин, ви вже пишете код так, щоб у майбутньому він не став «заблокованою цеглиною».
Є й ще одна практична причина любити delay: він змушує вас тримати очікування в suspend-шарі. А це автоматично робить код чистішим. За сигнатурою функції видно, що всередині може бути очікування.
Мінінавичка налагодження: «перевіряємо, хто і як чекає»
У цьому розділі ми потренуємося розуміти очікування не лише очима, а й цифрами. Коли ви починаєте писати корутинний код, часто зʼявляється відчуття: «ніби працює, але якось загадково». Найпростіший спосіб прибрати містичність — вимірювати час і друкувати проміжні етапи.
Зробімо утиліту, яка друкує час, що минув (це не специфічно для корутин, але дуже допомагає):
fun log(startMs: Long, message: String) {
val delta = System.currentTimeMillis() - startMs
println("[+$delta ms] $message")
}
Тепер використаємо її з delay:
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val start = System.currentTimeMillis()
log(start, "Почали") // [+0 ms] Почали
delay(200)
log(start, "Після delay") // [+200..250 ms] Після delay
}
І з Thread.sleep:
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
val start = System.currentTimeMillis()
log(start, "Почали") // [+0 ms] Почали
Thread.sleep(200)
log(start, "Після sleep") // [+200..250 ms] Після sleep
}
Знову ж таки: ззовні схоже. Але тепер ви чітко бачите, де саме стоїть очікування, і можете дисципліновано виносити його в suspend-функції.
5. Типові помилки під час роботи з suspend, delay і Thread.sleep
Помилка №1: спроба викликати delay() зі звичайної функції й «якось обійти компілятор».
Новачки іноді починають шукати обхідні шляхи: «а чи можна зробити delay без suspend?». Технічно можна (через погані трюки), але тоді ви руйнуєте головний сенс корутин: контракт очікування має бути видимим у сигнатурі. Правильне виправлення майже завжди одне й те саме: або позначити функцію як suspend, або перенести очікування туди, де вже є корутинний контекст (наприклад, усередину блока runBlocking).
Помилка №2: використання Thread.sleep() усередині корутинного коду, «бо так же простіше».
Це найпоширеніша звичка з Java: поставити Thread.sleep і жити спокійно. Проблема в тому, що такий код блокує потік. Поки ви навчаєтеся, це може не «вибухнути», але щойно очікувань стане багато або зʼявляться інші завдання, програма почне «гальмувати» зовсім неочевидно. Якщо ви хочете саме «почекати» в корутинному сценарії — беріть delay.
Помилка №3: додавати suspend «про всяк випадок», коли незрозуміло навіщо.
Іноді студенти починають ставити suspend всюди, ніби це «режим корутинного коду». Але suspend — не прикраса й не прискорювач, а зміна контракту функції: її не можна викликати зі звичайного коду напряму. Хороший критерій простий: додавайте suspend тоді, коли всередині функції справді є delay (або інші suspend‑операції), або ви свідомо хочете дозволити їй призупинятися.
Помилка №4: очікування, що delay сам по собі зробить виконання паралельним.
delay — це очікування, а не запуск додаткової роботи. Якщо ваша програма виконує одну операцію за іншою, то вона так і виконуватиме одну за іншою — просто очікування буде оформлене коректно. Це нормально: ми зараз учимося «чекати правильно», а не «робити все одночасно».
Помилка №5: змішування блокувального й неблокувального очікування в одній і тій самій логіці без чітких меж.
Коли в одному місці у вас delay, в іншому — Thread.sleep, а навколо ще введення/виведення й команди, поведінка стає важко передбачуваною. Краще дотримуватися дисципліни: якщо ви вже в корутинному сценарії, використовуйте корутинні інструменти очікування; якщо ви пишете повністю звичайний синхронний код — то принаймні свідомо памʼятайте, що Thread.sleep блокує потік.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ