CoroutineScope — что это, область жизни и работа в корутинах

Автор: IT Sectr Опубликовано: 2026-06-22 Время чтения: 10 мин

CoroutineScope — это интерфейс Kotlin, который определяет область жизни сопрограммы и предоставляет контекст для запуска новых корутин. По данным документации Kotlin, 2025, каждый экземпляр CoroutineScope содержит CoroutineContext и управляет всеми запущенными в нём корутинами. Когда scope завершается (cancel), все дочерние корутины автоматически отменяются, что предотвращает утечки памяти.

Главное

  • CoroutineScope — интерфейс с единственным полем CoroutineContext, определяющий жизненный цикл корутин
  • Job — элемент контекста, отвечающий за отмену: отмена scope отменяет все дочерние корутины
  • Структурная конкуренция — принцип, при котором дочерние корутины привязаны к родительскому scope
  • GlobalScope — scope на всё приложение, который не рекомендуется использовать из-за риска утечек
  • supervisorScope — специальный scope, в котором отмена одной дочерней корутины не отменяет остальные

Что такое CoroutineScope в Kotlin?

CoroutineScope — это фундаментальный интерфейс из библиотеки kotlinx.coroutines, который служит контейнером для корутин. Он определяет границы жизни сопрограмм: когда scope завершается, все корутины внутри него автоматически отменяются.

kotlin
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Интерфейс содержит всего одно поле — coroutineContext. Через него scope предоставляет диспетчер (Dispatcher), задачу (Job), обработчик исключений и другие элементы контекста для всех запущенных в нём корутин.

Роль в библиотеке kotlinx.coroutines

Все функции запуска корутин — launch, async, runBlocking — являются extension-функциями на CoroutineScope. Это означает, что вызвать их можно только при наличии объекта scope. Такой дизайн гарантирует, что каждая корутина имеет чётко определённого родителя и жизненный цикл.

Где применяется CoroutineScope

В Android каждый архитектурный компонент имеет свой scope: viewModelScope для ViewModel, lifecycleScope для Activity/Fragment. В серверных приложениях scope может быть привязан к HTTP-запросу или к пулу соединений с базой данных.

Как работает CoroutineScope: Job и структурная конкуренция

Понимание внутреннего устройства CoroutineScope требует знакомства с концепцией Job и принципом структурной конкуренции.

Job — задача корутины

Каждая корутина при запуске возвращает объект Job (или Deferred для async). Job представляет собой задачу с конечным жизненным циклом: New, Active, Completing, Completed, Cancelling, Cancelled. Job-объекты образуют древовидную структуру:

  • Родительский Job — scope, в котором запущена корутина
  • Дочерний Job — каждая корутина, запущенная через launch/async
  • Отмена родителя → отмена всех детей
  • Исключение в ребёнке → отмена родителя (кроме supervisorScope)

Принцип структурной конкуренции

Структурная конкуренция — ключевой архитектурный принцип Kotlin Coroutines, при котором время жизни корутины привязано к времени жизни её scope. Это контрастирует с моделью «fire-and-forget», где корутина продолжает жить после завершения scope. Преимущества структурной конкуренции:

  • Предсказуемый жизненный цикл — когда scope завершается, все корутины гарантированно остановлены
  • Автоматическая обработка ошибок — исключение в любой дочерней корутине распространяется на scope
  • Отсутствие утечек — ни одна корутина не остаётся висеть после завершения scope
  • Чёткая иерархия — код отражает логическую структуру параллельных операций

Жизненный цикл CoroutineScope

Когда вызывается scope.cancel(), Job scope переходит в состояние Cancelled, что рекурсивно отменяет все дочерние Job. После отмены scope может быть переиспользован только если создать новый экземпляр CoroutineScope.

Создание и настройка CoroutineScope

Создать CoroutineScope можно через фабричную функцию или через имплементацию интерфейса в своём классе. Рассмотрим оба подхода.

Фабричная функция CoroutineScope()

kotlin
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

scope.launch {
    println("Running on ${Thread.currentThread().name}")
}

Фабричная функция принимает CoroutineContext и создаёт scope с указанным контекстом. В примере используется Dispatchers.Default для CPU-интенсивных задач и SupervisorJob, который изолирует исключения между дочерними корутинами.

Имплементация интерфейса через composition

kotlin
class MyRepository {
    private val scope = CoroutineScope(Dispatchers.IO + Job())

    suspend fun fetchData(): Data = scope.async {
        api.getData()
    }.await()

    fun cleanup() {
        scope.cancel()
    }
}

Мы храним scope как поле класса и вручную вызываем cleanup для его отмены. Это подходит для компонентов с управляемым жизненным циклом — например, для репозиториев или менеджеров.

Имплементация через делегирование

Kotlin позволяет делегировать имплементацию CoroutineScope через ключевое слово by:

kotlin
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
    fun load() {
        launch {
            // coroutine runs in DataLoader scope
        }
    }
}

Такой подход удобен, когда класс сам является scope и хочет предоставлять методы запуска корутин. Однако будьте осторожны: класс наследует все методы CoroutineScope, включая cancel, что может нарушить инкапсуляцию.

GlobalScope против кастомного CoroutineScope

GlobalScope — это синглтон CoroutineScope для всего приложения. Его использование в production-коде официально не рекомендуется.

Проблемы GlobalScope

  • Отсутствие структурной конкуренции — корутины в GlobalScope не привязаны к жизненному циклу компонента
  • Утечки памяти — корутина может продолжать выполняться после закрытия Activity/Fragment
  • Затруднённое тестирование — GlobalScope нельзя заменить в тестах
  • Неконтролируемое потребление ресурсов — множество корутин могут работать дольше, чем ожидается

Когда GlobalScope оправдан

JetBrains допускает GlobalScope только в редких сценариях: фоновые процессы на уровне приложения, которые должны жить даже после закрытия всех Activity (например, синхронизация данных, аналитика). Но и в этих случаях предпочтительнее создать собственный scope с CoroutineScope(SupervisorJob()).

Рекомендация

Всегда используйте кастомный CoroutineScope с явным управлением жизненным циклом. В Android это viewModelScope и lifecycleScope. В серверных приложениях создавайте scope для каждого запроса или пула соединений.

coroutineScope vs supervisorScope: в чём разница

Обе функции являются suspend-функциями, которые создают временный scope для параллельных задач, но их поведение при исключениях принципиально различается.

ХарактеристикаcoroutineScopesupervisorScope
Поведение при ошибкеИсключение в дочерней корутине отменяет все остальныеИсключение в дочерней корутине НЕ отменяет остальные
Проброс ошибкиДа, первое исключение пробрасывается наружуДа, первое исключение пробрасывается наружу
Job по умолчаниюJob() — дочерние привязаны к родителюSupervisorJob() — дочери не зависят друг от друга
Типичный use-caseАтомарная операция из нескольких шаговНезависимые параллельные задачи (UI-загрузки)

Когда выбирать coroutineScope

Используйте coroutineScope, когда несколько параллельных операций образуют единую атомарную операцию. Например, загрузка данных с трёх серверов: если один запрос упал, остальные не имеют смысла.

kotlin
suspend fun loadProductPage(): ProductPage = coroutineScope {
    val product = async { api.getProduct() }
    val reviews = async { api.getReviews() }
    ProductPage(product.await(), reviews.await())
}

Если getProduct или getReviews выбрасывают исключение — обе корутины отменяются, а исключение пробрасывается вызывающему коду.

Когда выбирать supervisorScope

Используйте supervisorScope, когда параллельные операции не зависят друг от друга. Например, загрузка данных профиля в нескольких независимых секциях: если секция рекомендаций упала, заголовок профиля и список друзей должны отобразиться.

Типовые ошибки при работе с CoroutineScope

Рассмотрим наиболее частые ошибки разработчиков при использовании CoroutineScope в Kotlin.

Ошибка 1: Забыли отменить scope

Самый распространённый сценарий утечки корутины — создание scope без вызова cancel при завершении компонента. Если scope не отменён, корутины продолжают работать, удерживая ссылки на объекты. В Android используйте viewModelScope или lifecycleScope, которые отменяются автоматически.

Ошибка 2: Использование GlobalScope в Activity или Fragment

GlobalScope игнорирует жизненный цикл Android-компонентов. Корутина, запущенная в GlobalScope после закрытия Activity, продолжит выполнение и попытается обновить UI — что приведёт к краху. Всегда используйте lifecycleScope для UI-компонентов.

Ошибка 3: Переиспользование отменённого scope

После вызова cancel() scope нельзя переиспользовать — все корутины в нём уже завершены. Создайте новый экземпляр CoroutineScope через фабричную функцию. Job() не поддерживает повторную активацию.

Ошибка 4: Неправильное делегирование интерфейса CoroutineScope

При делегировании by класс получает публичный метод cancel(), который может быть вызван из любого места, нарушив инкапсуляцию. Храните scope приватным полем, а не делегируйте интерфейс.

Часто задаваемые вопросы

Чем отличается CoroutineScope от CoroutineContext?

CoroutineScope — это интерфейс, владеющий CoroutineContext и отвечающий за жизненный цикл корутин. CoroutineContext — это набор элементов (диспетчер, job, обработчик ошибок), определяющий «как» выполняется корутина. Одно из различий: scope создаёт корутины, context управляет их поведением.

Можно ли создать CoroutineScope с SupervisorJob?

Да, это стандартный паттерн: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob предотвращает каскадную отмену дочерних корутин при исключении в одной из них. Это полезно для независимых параллельных задач, где ошибка в одной не должна останавливать другие.

Сколько корутин может содержать CoroutineScope?

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

Как протестировать код с CoroutineScope?

Правильный способ — передавать scope в класс через конструктор или использовать runBlockingTest / runTest из kotlinx-coroutines-test. В тестах можно заменить scope на TestCoroutineDispatcher и контролировать выполнение корутин вручную.

Может ли одна корутина иметь свой собственный scope?

Нет, scope — это внешний контейнер для корутины. Сама корутина не является scope. Однако внутри корутины можно создать новый scope через coroutineScope или supervisorScope для параллельного запуска дочерних корутин.

Итоги

  • CoroutineScope — интерфейс с полем coroutineContext, определяющий жизненный цикл запущенных в нём сопрограмм
  • Структурная конкуренция — отмена scope автоматически отменяет все дочерние корутины, предотвращая утечки памяти
  • Job и SupervisiorJob — два режима обработки ошибок: каскадная отмена (Job) и изолированные ошибки (SupervisorJob)
  • GlobalScope — не рекомендуется для production из-за отсутствия привязки к жизненному циклу
  • coroutineScope vs supervisorScope — атомарные параллельные операции против независимых параллельных задач
  • viewModelScope и lifecycleScope — готовые scope для Android, отменяемые автоматически при завершении компонента
  • Фабричная функция — предпочтительный способ создания scope через CoroutineContext + явный вызов cancel

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также