Avatar ·

В чем разница между runBlocking и coroutineScope в Kotlin Coroutines?

Привет, коллеги! Я начал изучать корутины в Kotlin и наткнулся на две функции, которые кажутся мне очень похожими: runBlocking и coroutineScope. Я читал документацию, но для меня их поведение до сих пор не очевидно.

Вот пример, который я написал:

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

И другой:

fun main() = runBlocking {
    coroutineScope {
        launch {
            delay(1000L)
            println("World!")
        }
        println("Hello,")
    }
}

Оба примера выводят сначала "Hello,", а через секунду "World!". Но я подозреваю, что разница есть, и она важна. В чем она заключается?

Конкретно меня интересует:

  • Почему runBlocking блокирует текущий поток, а coroutineScope — нет?
  • Как эти функции ведут себя в реальных приложениях, например, в Android или серверном коде?
  • Когда какую из них лучше использовать?

Буду благодарен за подробное объяснение с примерами.

<<>>

Ключевое различие: блокировка потока vs приостановка корутины

Разница между runBlocking и coroutineScope фундаментальна и связана с тем, как они взаимодействуют с потоком выполнения.

1. runBlocking — блокирует поток

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

Пример с ошибкой:

fun main() {
    runBlocking {
        delay(1000L) // Блокирует main поток на 1 секунду
        println("Inside blocking")
    }
    println("After runBlocking") // Выполнится только после завершения runBlocking
}

Когда использовать:

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

Никогда не используйте runBlocking в продакшене Android или серверных приложениях — это заблокирует UI-поток или поток обработки запроса, что приведет к зависанию.

2. coroutineScope — приостанавливает корутину

coroutineScope — это suspend-функция. Она не блокирует поток, а приостанавливает текущую корутину до завершения всех дочерних корутин. Поток, на котором выполняется корутина, освобождается для других задач.

Пример:

suspend fun doWork() = coroutineScope {
    launch {
        delay(1000L)
        println("Child coroutine finished")
    }
    println("Scope started")
} // Здесь корутина приостановится, но поток не заблокирован

Когда использовать:

  • Внутри suspend функций, чтобы выполнить несколько параллельных задач и дождаться их всех.
  • Для структурированной конкурентности — все дочерние корутины автоматически отменяются, если родительская корутина отменяется.
  • В Android (ViewModel, Activity) и серверных приложениях (Ktor, Spring WebFlux) — это основной способ работы с корутинами.

3. Практический пример: разница в поведении

Рассмотрим код, который показывает разницу:

fun main() = runBlocking {
    println("Before coroutineScope")

    coroutineScope {
        launch {
            delay(1000L)
            println("Task 1 done")
        }
        launch {
            delay(500L)
            println("Task 2 done")
        }
        println("Inside coroutineScope")
    }

    println("After coroutineScope") // Выполнится только после завершения всех дочерних корутин

    // А теперь runBlocking без coroutineScope:
    runBlocking {
        launch {
            delay(1000L)
            println("Task 3 done")
        }
        launch {
            delay(500L)
            println("Task 4 done")
        }
        println("Inside runBlocking")
    }
    println("After runBlocking")
}

Вывод будет:

Before coroutineScope
Inside coroutineScope
Task 2 done
Task 1 done
After coroutineScope
Inside runBlocking
Task 4 done
Task 3 done
After runBlocking

Обратите внимание: coroutineScope ведет себя как обычная suspend-функция — она приостанавливает выполнение дочерней корутины runBlocking, но не блокирует поток. runBlocking же блокирует поток, и второй вызов runBlocking не может начаться, пока первый не завершится.

4. Итоговая таблица

Характеристика runBlocking coroutineScope
Тип Обычная функция (не suspend) Suspend-функция
Блокирует поток? Да Нет (приостанавливает корутину)
Где использовать main(), тесты, bridge Внутри suspend-функций, ViewModel, серверов
Отмена дочерних Нет автоматической отмены Да (структурированная конкурентность)

Золотое правило: используйте coroutineScope везде, где это возможно, и runBlocking только как крайнее средство для интеграции с блокирующим кодом.

<<>>

Дополнение: практические сценарии использования

Хочу добавить несколько практических советов, которые помогут избежать ошибок.

1. runBlocking в тестах

В юнит-тестах runBlocking — это стандартный способ тестировать suspend-функции:

@Test
fun testMySuspendFunction() = runBlocking {
    val result = mySuspendFunction()
    assertEquals(expected, result)
}

2. coroutineScope для параллельных задач

Используйте coroutineScope для выполнения нескольких задач одновременно и ожидания всех результатов:

suspend fun fetchData(): Data = coroutineScope {
    val deferred1 = async { api1.fetch() }
    val deferred2 = async { api2.fetch() }
    Data(deferred1.await(), deferred2.await())
}

3. Ошибка: runBlocking внутри coroutineScope

Избегайте вложенных runBlocking — это может привести к блокировке потоков и проблемам с производительностью:

// ПЛОХО
suspend fun bad() = coroutineScope {
    runBlocking { // Блокирует поток! Не делайте так
        delay(1000)
    }
}

// ХОРОШО
suspend fun good() = coroutineScope {
    delay(1000) // Просто используем delay
}

4. Исключения и отмена

В coroutineScope если одна дочерняя корутина выбрасывает исключение, все остальные автоматически отменяются. В runBlocking такого не происходит — исключение распространяется на родительский поток.

Помните: coroutineScope — это инструмент для структурированной конкурентности, а runBlocking — это костыль для интеграции с блокирующим миром.