Что такое structured concurrency?
Structured concurrency (структурированная конкуренция) — это концепция управления параллелизмом и жизненным циклом корутин, основанная на том, что все запущенные асинхронные задачи должны быть частью чётко определённой структуры, соответствующей области действия, в которой они были запущены. Эта идея обеспечивает предсказуемость, безопасность и управляемость параллельного выполнения.
Основные идеи structured concurrency
-
Корутины "привязаны" к области (scope):
Все корутины запускаются внутри определённого CoroutineScope, например, lifecycleScope, viewModelScope или GlobalScope. Они живут до тех пор, пока живёт этот scope. -
Родитель отвечает за дочерние задачи:
Если родительская корутина отменяется, все её дочерние корутины также отменяются. Родитель не завершится, пока не завершатся все дочерние задачи. -
Гарантия завершения:
Корутина и весь её 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 позволяет организовать асинхронный код так же надёжно, как синхронный: каждая задача находится внутри своей "структуры", родитель отвечает за детей, отмена передаётся вниз по иерархии, исключения обрабатываются централизованно. Это делает корутины безопасным и мощным инструментом при создании сложных многозадачных приложений.