Что такое structured concurrency?

Structured concurrency (структурированная конкуренция) — это концепция управления параллелизмом и жизненным циклом корутин, основанная на том, что все запущенные асинхронные задачи должны быть частью чётко определённой структуры, соответствующей области действия, в которой они были запущены. Эта идея обеспечивает предсказуемость, безопасность и управляемость параллельного выполнения.

Основные идеи structured concurrency

  1. Корутины "привязаны" к области (scope):
    Все корутины запускаются внутри определённого CoroutineScope, например, lifecycleScope, viewModelScope или GlobalScope. Они живут до тех пор, пока живёт этот scope.

  2. Родитель отвечает за дочерние задачи:
    Если родительская корутина отменяется, все её дочерние корутины также отменяются. Родитель не завершится, пока не завершатся все дочерние задачи.

  3. Гарантия завершения:
    Корутина и весь её scope не завершатся, пока все вложенные корутины не завершатся или не будут отменены. Это позволяет избежать утечек, "зависших" задач и гонок.

Примеры structured concurrency

Пример 1: Простой scope

fun CoroutineScope.fetchData() = launch {
val user = async { fetchUser() }
val posts = async { fetchPosts() }
println("User: ${user.await()}, Posts: ${posts.await()}")
}

Обе корутины запускаются внутри одного CoroutineScope. Если одна из них выбросит исключение, вторая будет автоматически отменена. И вся родительская задача тоже.

Пример 2: Без structured concurrency (GlobalScope)

fun doWork() {
GlobalScope.launch {
delay(1000)
println("Independent coroutine")
}
}

Здесь корутина непривязана к жизненному циклу вызвавшего контекста. Если, например, Activity будет уничтожена, корутина продолжит выполняться. Это может привести к утечкам памяти, крашам, нежелательному поведению. Именно это structured concurrency предотвращает.

Пример 3: Правильная привязка через CoroutineScope

fun CoroutineScope.doWorkSafely() {
launch {
val result = async { loadSomething() }
println(result.await())
}
}

Теперь все корутины — часть структуры. Если launch отменится, async тоже завершится, и loadSomething будет прерван.

Контекст иерархии

Каждая корутина создаёт иерархию задач, где каждая вложенная корутина становится дочерней:

CoroutineScope
└── launch (parent)
├── async (child1)
└── async (child2)

Если родитель отменяется, все дети тоже отменяются. Если child1 выбрасывает исключение — всё «дерево» тоже отменяется.

Structured concurrency в Android

lifecycleScope

lifecycleScope.launch {
// отменится при уничтожении Activity/Fragment
}

viewModelScope

viewModelScope.launch {
// отменится при уничтожении ViewModel
}

Эти scope обеспечивают структурированную конкуренцию — автоматически управляют жизненным циклом корутин и предотвращают утечки.

Structured concurrency в withContext

withContext() также участвует в структуре. Он приостанавливает текущую корутину, переключает контекст, выполняет код, и потом возвращает управление:

val data = withContext(Dispatchers.IO) {
loadDataFromDisk()
}

Если текущая корутина будет отменена до завершения loadDataFromDisk(), то и withContext отменится — всё строго иерархично.

Structured concurrency vs Unstructured concurrency

Функция / Поведение Structured concurrency Unstructured concurrency
Привязка задач к родительскому scope Есть Нет
--- --- ---
Автоматическая отмена дочерних задач Да Нет
--- --- ---
Управление временем жизни задачи Централизованное Разрозненное
--- --- ---
Утечки памяти и гонки Минимальны Часто возникают
--- --- ---
Читаемость и отладка Высокая Сложная
--- --- ---
Пример API viewModelScope.launch {} GlobalScope.launch {}
--- --- ---

Structured concurrency внутри supervisorScope

В некоторых случаях нужно, чтобы отмена одной корутины не приводила к отмене других. Тогда используется supervisorScope, который изолирует исключения дочерних корутин:

coroutineScope {
supervisorScope {
val child1 = launch {
throw RuntimeException("Failure in child1")
}
val child2 = launch {
delay(1000)
println("Child2 completed")
}
}
}

Здесь child2 завершится, даже если child1 упал. Это тоже часть структурированной конкуренции, но с другим поведением в отношении исключений.

Использование structured concurrency с CoroutineScope в своих классах

Если ты пишешь собственные классы (например, репозиторий, сервис), ты можешь ввести свой CoroutineScope:

class MyRepository {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun fetch() {
scope.launch {
// структура задач внутри
}
}
fun clear() {
scope.cancel() // отмена всей структуры
}
}

Это даёт полный контроль над иерархией задач и отменой.

Structured concurrency позволяет организовать асинхронный код так же надёжно, как синхронный: каждая задача находится внутри своей "структуры", родитель отвечает за детей, отмена передаётся вниз по иерархии, исключения обрабатываются централизованно. Это делает корутины безопасным и мощным инструментом при создании сложных многозадачных приложений.