JavaRush /Курсы /Kotlin SELF /runBlocking в консольном main: корректный запуск корутин

runBlocking в консольном main: корректный запуск корутин

Kotlin SELF
52 уровень , 1 лекция
Открыта

1. Почему из main нельзя вызвать suspend

Если вы впервые видите корутины, то момент с suspend обычно вызывает лёгкое удивление. Примерно такое: «Ну я же просто хочу вызвать функцию, почему компилятор против?» И компилятор, как строгий охранник в клубе, отвечает: «Паспорт показываем. У вас корутинного контекста нет — проход запрещён».

Смысл в том, что suspend — это изменение контракта функции. Такая функция может приостанавливаться, а значит ей нужен «менеджер», который умеет ставить корутину на паузу и продолжать. В Kotlin этот менеджер появляется, когда вы запускаете корутинный блок выполнения (мы сегодня как раз научимся это делать).

Посмотрим на типичную ситуацию и на типичную ошибку.

suspend fun loadUserName(): String {
    return "Alice"
}

fun main() {
    val name = loadUserName() // <-- так нельзя
    println(name)
}

Компилятор скажет примерно следующее (формулировки могут отличаться, но смысл один):
Suspend function 'loadUserName' should be called only from a coroutine or another suspend function.

И это отлично: компилятор не даёт вам сделать код, который потом «случайно» будет вести себя странно. Он заставляет вас явно обозначить границу: вот здесь начинается корутинный сценарий, а вот здесь — обычный код.

2. runBlocking как мост в корутины

Теперь к главному герою лекции. runBlocking — это функция из kotlinx.coroutines, которая позволяет запустить корутинный код из обычной функции. Она делает две важные вещи одновременно.

Во-первых, она создаёт корутинное окружение, внутри которого можно вызывать suspend-функции. Во-вторых, она блокирует текущий поток, пока её блок не завершится. Это звучит как «предательство идеалов неблокирующего будущего», но для консольного main это как раз нормально: мы хотим, чтобы программа не завершилась, пока сценарий не отработал. (Если main завершится раньше — вы просто не увидите половину действий.)

Минимальный каркас выглядит так:

import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    println("Мы внутри runBlocking") // Мы внутри runBlocking
}

Обратите внимание на форму fun main() = .... Это просто компактная запись (expression body).

Если вам психологически спокойнее видеть фигурные скобки у main (некоторым так проще читать), можно и так:

import kotlinx.coroutines.runBlocking

fun main() {
    runBlocking {
        println("Снова внутри runBlocking") // Снова внутри runBlocking
    }
}

Обе формы корректны. В учебных примерах часто удобнее первая, потому что сразу видно: точка входа — это runBlocking.

3. Подключение kotlinx.coroutines

Перед тем как писать runBlocking, важно проговорить очевидную (и часто забываемую) вещь: runBlocking живёт не в стандартной библиотеке Kotlin, а в kotlinx.coroutines. То есть проект должен иметь зависимость на корутины, иначе IDE просто скажет Unresolved reference: runBlocking.

Логика такая: добавляем зависимость в build.gradle.kts, синхронизируем проект, импортируем нужные функции. Версия зависит от вашего шаблона проекта и настроек курса, поэтому здесь — схема (идея), без привязки к конкретному числу.

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:VERSION")
}

Дальше в коде:

import kotlinx.coroutines.runBlocking

И вот теперь runBlocking становится «легальным».

4. runBlocking блокирует текущий поток

Название runBlocking честное до грусти: да, оно блокирует. Именно поэтому в консольных приложениях мы используем runBlocking как точку входа, а не как «удобную штуку, которую можно вызывать где угодно».

Чтобы почувствовать это руками, достаточно такого примера:

import kotlinx.coroutines.runBlocking

fun main() {
    println("Before") // Before
    runBlocking {
        println("Inside") // Inside
    }
    println("After") // After
}

Логика выполнения строго последовательная: Before → Inside → After. Между Before и After поток занят выполнением блока runBlocking. Это не «параллельность», не «фон» и не «тайная корутина, которая живёт отдельно». Это мост, который держит main на месте, пока корутинный сценарий не закончит работу.

Если вам нужна простая бытовая аналогия, то runBlocking — это турникет в метро. Пока вы внутри турникета, вы не можете «быстро сбегать за кофе и вернуться» — сначала проходите до конца, и только потом жизнь продолжается.

С точки зрения архитектуры консольного приложения это очень удобно: программа должна жить ровно столько, сколько живёт основной сценарий.

5. runBlocking как выражение и возврат значения

Супер-практичная фишка: runBlocking можно использовать не только ради «выполнить блок», но и как выражение, возвращающее значение. Это позволяет сделать мост «туда и обратно»: вызвать suspend-функцию и получить обычное значение, которое дальше будет жить в обычном коде.

Пример с suspend-функцией, которая возвращает Int:

import kotlinx.coroutines.runBlocking

suspend fun answer(): Int {
    return 42
}

fun main() {
    val x: Int = runBlocking { answer() }
    println(x) // 42
}

Здесь важно уловить мысль: runBlocking { ... } — это не «специальный синтаксис», а обычная функция, которая возвращает результат последней строки блока. То есть это прямо как if-выражение или try-выражение, только с корутинами.

6. Один runBlocking в main и suspend-сценарий

Давайте теперь сделаем шаг к стилю, который реально спасает начинающих от хаоса. Типичная ошибка новичка — начать вставлять runBlocking куда попало: «О, мне тут нужен suspend-вызов, заверну-ка я его в runBlocking». Проблема в том, что так вы размазываете границу «обычный код ↔ корутинный код» по всему проекту, и через неделю сами не сможете объяснить, где у вас какая логика.

Правильнее держать один вход в корутинный сценарий — в main, а затем вызывать suspend-функции по цепочке.

Сделаем это на мини-практическом примере консольного приложения. Представим, что у нас есть учебный CLI-проект (условно назовём его BudgetBuddy): он хранит расходы и печатает отчёты. Сегодня мы ничего «асинхронного» ещё не делаем — мы просто готовим архитектуру так, чтобы завтра добавить корректные ожидания и параллельность.

Сначала — модели (коротко):

data class Expense(
    val amount: Int,
    val category: String
)

Теперь — корутинный сценарий приложения. Важно: main маленький, а вся логика в suspend fun runApp().

import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    runApp()
}

suspend fun runApp() {
    println("BudgetBuddy стартовал") // BudgetBuddy стартовал
}

Пока это выглядит как «переложили одну строку в другую функцию», но это очень правильная заготовка. Как только внутри runApp() появится ожидание или вызовы suspend-API, вы не будете перестраивать весь проект — контракт уже честный.

7. Пример: грузим данные и печатаем отчёт

Теперь добавим чуть больше «жизни», но без тем следующей лекции (мы пока не делаем delay, не запускаем launch/async, не выбираем Dispatchers). Наша цель — увидеть, как runBlocking позволяет вызывать suspend-функции, даже если они пока «не приостанавливаются».

Сделаем функцию загрузки данных как suspend (в будущем она может читать файл/БД/сеть, а сегодня просто вернёт заглушку):

suspend fun loadInitialExpenses(): List<Expense> {
    return listOf(
        Expense(amount = 1200, category = "Food"),
        Expense(amount = 3500, category = "Rent"),
        Expense(amount = 800, category = "Coffee")
    )
}

И функцию формирования строки отчёта (обычная функция, потому что она просто вычисляет строку):

fun formatReport(expenses: List<Expense>): String {
    val total = expenses.sumOf { it.amount }
    return "Всего расходов: $total"
}

Соберём сценарий в runApp():

import kotlinx.coroutines.runBlocking

fun main() = runBlocking {
    runApp()
}

suspend fun runApp() {
    val expenses = loadInitialExpenses()
    val report = formatReport(expenses)

    println(report) // Всего расходов: 5500
}

Проверьте глазами: код читается почти как «обычный последовательный». И это как раз одна из причин, почему корутины любят: они позволяют писать линейный читаемый сценарий, но при этом оставляют возможность ожидать «долгие операции» честно и безопасно.

8. Поток выполнения и стиль использования runBlocking

Схема исполнения

Прежде чем двигаться дальше по дню, полезно закрепить модель выполнения именно для консольного случая. Сейчас у вас есть такая цепочка: JVM вызывает main, main создаёт корутинное окружение через runBlocking, выполняет блок и только потом завершает программу.

Это можно представить так:

flowchart TD
    A["JVM вызывает main()"] --> B["main() вызывает runBlocking { ... }"]
    B --> C["Внутри блока: suspend-сценарий runApp()"]
    C --> D["Сценарий завершается"]
    D --> E["runBlocking возвращает управление main()"]
    E --> F["main() завершается, программа заканчивается"]

Смысл диаграммы в одной фразе: runBlocking — это «контролируемая дверь» в корутинный сценарий, и пока сценарий не закончится, консольная программа не закрывается.

Где заканчивается польза runBlocking

Очень хочется «применять runBlocking везде, где неудобно». Но именно в этот момент он превращается в антипаттерн. Его сила — в ясной точке входа, когда у вас есть внешний мир (обычные функции, JVM, main) и внутренний мир (suspend-сценарии).

Для консольного приложения типичный здоровый стиль выглядит так: main — это тонкая рамка; всё, что похоже на «сценарий» или «долгая операция», оформляется как suspend-функции, которые вызываются внутри единственного runBlocking.

А ещё полезно помнить, что Kotlin в принципе умеет иметь suspend fun main(), и в официальных примерах корутин можно встретить именно такую форму, где main сам является suspend. Но в рамках нашего курса мы держимся runBlocking, потому что он проще объясняется, явно показывает семантику блокировки, и хорошо подходит именно для консольной программы как «мостик».

9. Типичные ошибки при работе с runBlocking

Ошибка №1: думать, что runBlocking «не блокирует», потому что это корутины.
Название тут максимально честное: runBlocking блокирует текущий поток до завершения блока. Это нормально в main, но если вы начинаете использовать его внутри бизнес-логики «просто чтобы вызвать suspend», вы возвращаете себе старую проблему блокировок, только в новой упаковке.

Ошибка №2: использовать много runBlocking в разных местах проекта «для удобства».
Так появляется несколько разрозненных входов в корутинный мир, и вы теряете контроль над тем, где у вас обычный код, а где корутинный. Чаще всего правильнее сделать один runBlocking в main, а дальше построить цепочку suspend-функций, чтобы контракт был единым и читаемым.

Ошибка №3: пытаться вызвать suspend-функцию из обычной функции и «лечить» это runBlocking прямо там.
Это очень соблазнительно: «ну мне же только один раз надо». Но потом «один раз» становится десять раз, а потом вы удивляетесь, почему программа стала тормозить и почему у вас половина функций внезапно стала зависеть от корутин, хотя вы этого не планировали.

Ошибка №4: смешивать в main всё подряд: парсинг аргументов, ввод/вывод и корутинный сценарий в одном огромном блоке.
runBlocking не требует, чтобы вы писали «простыню». Наоборот: он позволяет красиво завернуть основной сценарий в отдельную функцию (runApp()), а main оставить тонкой рамкой. Так код легче читать, тестировать и расширять.

Ошибка №5: ожидать, что runBlocking сам по себе делает программу «асинхронной».
runBlocking — это способ зайти в корутинный код. Он не запускает параллельность автоматически и не делает ожидания «неблокирующими» по волшебству. Он просто даёт вам корутинное окружение и удерживает поток, пока сценарий не завершится. Всё остальное зависит от того, какие suspend-функции и какие корутинные механизмы вы используете внутри (это уже темы следующих лекций).

1
Задача
Kotlin SELF, 52 уровень, 1 лекция
Недоступна
Дверь корутин
Дверь корутин
1
Задача
Kotlin SELF, 52 уровень, 1 лекция
Недоступна
Генератор приветствия
Генератор приветствия
1
Задача
Kotlin SELF, 52 уровень, 1 лекция
Недоступна
Сценарий запуска
Сценарий запуска
1
Задача
Kotlin SELF, 52 уровень, 1 лекция
Недоступна
Ответ из капсулы
Ответ из капсулы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ