Kotlin CoroutinesJuniorTechnical

Что такое CoroutineScope и почему он важен?

CoroutineScope хранит CoroutineContext и управляет жизненным циклом корутин: при его отмене все дочерние корутины автоматически отменяются. Это основа структурированного параллелизма, защищающая от утечек.

CoroutineScope

CoroutineScope — это контейнер, который хранит CoroutineContext и определяет жизненный цикл корутин. Любая корутина, запущенная через launch или async, привязана к scope: если scope отменяется, все его дочерние корутины тоже отменяются. Это главный механизм предотвращения утечек корутин.

Зачем нужен scope

  • Управление жизненным циклом: отмена scope отменяет все запущенные в нём корутины.
  • Структурированный параллелизм: scope завершается только тогда, когда завершены все его дочерние корутины.
  • Контекст по умолчанию: хранит диспетчер, Job, имя и обработчик исключений для всех дочерних корутин.

Создание собственного scope

import kotlinx.coroutines.*

class DataRepository {
    // Собственный scope с SupervisorJob — падение одной задачи не отменяет остальные
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun loadData(id: Int) {
        scope.launch {
            val result = fetchFromNetwork(id)  // suspend-функция
            println("Loaded: $result")
        }
    }

    // Вызывается при уничтожении объекта
    fun cleanup() {
        scope.cancel()  // отменяет все активные корутины
    }

    private suspend fun fetchFromNetwork(id: Int): String {
        delay(100)  // имитация сетевого запроса
        return "Data#$id"
    }
}

fun main() = runBlocking {
    val repo = DataRepository()
    repo.loadData(1)
    repo.loadData(2)
    delay(200)
    repo.cleanup()
    println("Repository cleaned up")
}

coroutineScope vs CoroutineScope

coroutineScope { } (с маленькой буквы) — это suspend-функция-строитель, которая создаёт дочерний scope и ждёт завершения всех запущенных в нём корутин. Она не требует ручной отмены и автоматически распространяет исключения наверх.

suspend fun loadAll(): Pair<String, String> = coroutineScope {
    // оба запроса идут параллельно
    val a = async { fetchA() }
    val b = async { fetchB() }
    a.await() to b.await()
    // функция вернётся только когда оба async завершатся
}

suspend fun fetchA(): String { delay(100); return "A" }
suspend fun fetchB(): String { delay(150); return "B" }

GlobalScope — почему его избегают

GlobalScope существует на протяжении всего времени жизни приложения. Корутины, запущенные в нём, не привязаны ни к какому компоненту и не будут отменены при уничтожении экрана или объекта. Это ведёт к утечкам памяти и нежелательным фоновым операциям.

// Плохо — утечка: корутина живёт вечно
GlobalScope.launch {
    updateUi()  // ViewModel уже уничтожена, а корутина работает
}

// Хорошо — привязано к ViewModel
viewModelScope.launch {
    updateUi()
}

Встроенные scope в платформах

  • viewModelScope (Android) — отменяется при вызове onCleared().
  • lifecycleScope (Android) — отменяется при уничтожении Lifecycle-владельца.
  • MainScope() — scope с Dispatchers.Main и SupervisorJob(), для использования в UI-компонентах.

Подводные камни

  • Создание CoroutineScope(Job()) вместо CoroutineScope(SupervisorJob()): падение одной дочерней корутины отменяет весь scope и все остальные задачи.
  • Забыть вызвать scope.cancel() в onDestroy / onCleared — корутины продолжат работать после уничтожения компонента.
  • Использование GlobalScope в production-коде ради «удобства» — компилятор не предупреждает, но это источник утечек.
  • Передача scope как параметра функциям — нарушает инкапсуляцию и делает управление жизненным циклом непредсказуемым. Лучше передавать suspend-функции.
  • Путаница между CoroutineScope() (функция-фабрика, возвращает объект) и coroutineScope { } (suspend-строитель блока) — разные API с разной семантикой.
  • Запуск корутины вне scope (через runBlocking) в production-коде блокирует поток и не участвует в структурированном параллелизме.

Common mistakes

  • Объяснять «CoroutineScope» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Scope без жизненного цикла превращает фоновые задачи в утечки и теряет исключения.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «CoroutineScope» своими словами и связывает ее с кодом.
  • Называет механизм: Builders запускаются как extension functions на scope, поэтому дочерние задачи наследуют контекст и участвуют в structured concurrency.
  • Видит production-последствие: Scope без жизненного цикла превращает фоновые задачи в утечки и теряет исключения.

Sources

Related topics