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"]
Важно: это схема про корутину, не про поток. Поток — отдельная сущность, и он может быть свободен делать другое, пока корутина «спит».
3. 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 | Можно ли вызвать из обычной функции |
|---|---|---|---|---|
| Пауза в корутинном коде | |
Корутина | Да | Нет |
| Пауза где угодно | |
Поток | Нет | Да |
4. Практический паттерн: ожидание внутри 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-операциями, а вторая — всегда про блокировку потока.
5. Почему 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-функции.
6. Типичные ошибки при работе с 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 блокирует поток.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ