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("До") // До
    runBlocking {
        println("Усередині") // Усередині
    }
    println("Після") // Після
}

Логіка виконання чітко послідовна: До → Усередині → Після. Між До і Після потік зайнятий виконанням блока 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-функції та які корутинні механізми ви використовуєте всередині (це вже теми наступних лекцій).

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ