← Назад к вопросам

Как повлияет отмена одной корутины на остальные в рамках общего CoroutineScope

3.0 Senior🔥 142 комментариев
#Kotlin основы#Архитектура и паттерны#Многопоточность и асинхронность

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Отмена корутины в общем CoroutineScope: механизмы и последствия

В Kotlin Coroutines, когда вы отменяете одну корутину в рамках общего CoroutineScope, это не приводит к автоматической отмене всех остальных корутин в этом скоупе. Однако существуют важные нюансы и механизмы распространения отмены, которые зависят от структуры родительско-дочерних отношений и используемых Job объектов.

Ключевые принципы иерархии и отмены

  1. Родительско-дочерняя иерархия: Когда корутина запускается с помощью таких строителей, как launch или async, внутри другой корутины или CoroutineScope, она становится дочерней по отношению к родительской Job. Это создаёт иерархическую структуру.

  2. Распространение отмены "сверху вниз" (Parent-to-Child): Отмена родительской Job (или скоупа) автоматически отменяет все её дочерние корутины. Это основной механизм управления жизненным циклом.

  3. Нераспространение отмены "снизу вверх" (Child-to-Parent): Отмена одной дочерней корутины НЕ приводит к отмене её родителя или соседних дочерних корутин по умолчанию.

Практические примеры и код

Рассмотрим различные сценарии.

Сценарий 1: Независимые корутины в одном скоупе

Если корутины запущены как "братья" (siblings) в одном скоупе, отмена одной не затронет другие.

import kotlinx.coroutines.*

fun main() = runBlocking {
    val scope = CoroutineScope(Dispatchers.Default)

    val job1 = scope.launch {
        repeat(100) { i ->
            delay(100)
            println("Job1: $i")
        }
    }

    val job2 = scope.launch {
        repeat(100) { i ->
            delay(100)
            println("Job2: $i")
        }
    }

    delay(250) // Даём время начать выполнение
    job1.cancel() // Отменяем только первую корутину
    println("Job1 отменён!")

    delay(500) // Ждём, наблюдая за выполнением
    println("Job2 всё ещё жив: ${job2.isActive}")
    scope.cancel() // Теперь отменяем весь scope и job2
}

Вывод будет примерно таким:

Job1: 0
Job2: 0
Job1: 1
Job2: 1
Job1 отменён!
Job2: 2
Job2: 3
Job2: 4
Job2 всё ещё жив: true

Здесь job2 продолжил работу после отмены job1.

Сценарий 2: Дочерние корутины внутри родительской

Если корутины являются дочерними по отношению к общей родительской, отмена родителя остановит всех детей.

import kotlinx.coroutines.*

fun main() = runBlocking {
    // Родительская корутина
    val parentJob = launch {
        // Дочерняя корутина 1
        launch {
            repeat(100) { i ->
                delay(100)
                println("Child1: $i")
            }
        }
        // Дочерняя корутина 2
        launch {
            repeat(100) { i ->
                delay(100)
                println("Child2: $i")
            }
        }
    }

    delay(250)
    parentJob.cancel() // Отменяем родителя -> отменятся ВСЕ дочерние
    println("Родитель отменён")
    delay(500)
    println("Дети тоже отменены и больше не печатают")
}

Сценарий 3: Явное создание иерархии через Job()

Можно явно создать родительский Job и передать его в скоуп, чтобы получить контроль над группой корутин.

import kotlinx.coroutines.*

fun main() = runBlocking {
    // Создаём родительскую Job
    val parentJob = Job()
    val scope = CoroutineScope(Dispatchers.Default + parentJob)

    scope.launch {
        repeat(100) { i ->
            delay(100)
            println("Coroutine A: $i")
        }
    }

    scope.launch {
        repeat(100) { i ->
            delay(100)
            println("Coroutine B: $i")
        }
    }

    delay(250)
    parentJob.cancel() // Отменяем через родительскую Job -> отменяем весь scope
    println("Родительская Job отменена, все корутины в scope должны остановиться")
    delay(500)
}

Важные исключения и механизмы

  • Structured Concurrency: Этот принцип как раз поощряет создание иерархий "родитель-потомок", где отмена родителя автоматически отменяет всех потомков, предотвращая утечки корутин.
  • SupervisorJob: Это особый тип Job, который меняет поведение распространения отмены. При использовании SupervisorJob или supervisorScope неудача (failure) или отмена одной дочерней корутины не приводит к отмене родителя или других дочерних корутин. Однако явный вызов cancel() на самом SupervisorScope всё равно отменит всех его детей.

Выводы для разработчика

  1. Прямая отмена одной корутины не затрагивает "братьев" в общем CoroutineScope, если только они не связаны явной родительско-дочерней иерархией.
  2. Для группового управления (например, отмена всех операций на экране при закрытии ViewModel) используйте иерархию: создайте скоуп с собственной Job (val scope = CoroutineScope(Dispatchers.Main + Job())) и отменяйте её, что приведёт к каскадной отмене всех корутин, запущенных в этом скоупе.
  3. Structured Concurrency — ваш союзник: старайтесь запускать новые корутины внутри уже существующего скоупа (например, viewModelScope или lifecycleScope), а не создавать глобальные GlobalScope корутины. Это обеспечивает автоматическое и безопасное управление жизненным циклом.
  4. Для задач, где неудача одной не должна влиять на другие, рассмотрите использование SupervisorJob.

Таким образом, влияние отмены строго зависит от структуры, которую вы создаёте, и является мощным инструментом для управления параллельными операциями.