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 — са разширяващи функции на 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("Работи на ${Thread.currentThread().name}")
}

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

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

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 {
            // корутината работи в scope DataLoader
        }
    }
}

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

GlobalScope срещу персонализиран CoroutineScope

GlobalScope — е синглтон CoroutineScope за цялата приложения. Неговото използване в производствен код официално не се препоръчва.

Проблеми на GlobalScope

  • Липса на структурна конкуренция — корутините в GlobalScope не са свързани с жизнения цикъл на компонентата
  • Изтичане на памет — корутината може да продължи да работи след затварянето на Activity/Fragment
  • Трудно тестване — GlobalScope не може да бъде заменен в тестовете
  • Неконтролирана консумация на ресурси — много корутини могат да работят по-дълго от очакваното

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

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

Препоръка

Винаги използвайте персонализиран CoroutineScope с изрично управление на жизнения цикъл. В Android това са viewModelScope и lifecycleScope. В сървърни приложения създавайте scope за всяка заявка или пул от връзки.

coroutineScope vs supervisorScope: каква е разликата

И двете функции са suspend функции, които създават временен scope за паралелни задачи, но поведението им при изключения се различава съществено.

ХарактеристикаcoroutineScopesupervisorScope
Поведение при грешкаИзключение в дочерна корутина анулира всички останалиИзключение в дочерна корутина НЕ анулира останалите
Разпространение на грешкаДа, първото изключение се разпространява навънДа, първото изключение се разпространява навън
Подразбираем JobJob() — децата са свързани с родителяSupervisorJob() — децата не зависят едно от друго
Типичен случай на употребаАтомна операция от няколко стъпкиНезависими паралелни задачи (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 — е набор от елементи (dispatcher, job, обработка на грешки), който определя „как” се изпълнява корутината. Една от разликите: scope създава корутини, context управлява поведението им.

Може ли да се създаде CoroutineScope с SupervisorJob?

Да, това е стандартен модел: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob предотвратява каскадно анулиране на дочерните корутини при изключение в една от тях. Това е полезно за независими паралелни задачи, където грешка в здача не трябва да спира другите.

Колко корутини може да пъба CoroutineScope?

Няма ограничения за броя корутини в scope — те са ограничени само от наличната памет и настройките на dispatcher-а. Практическият лимит обикновено е хиляди активни корутини в един scope. Въпреки това, голям брой корутини може да показва архитектурни проблеми.

Как да тествам код с CoroutineScope?

Правилният начин е да предадете scope на класа чрез конструктора или да използвате runBlockingTest / runTest от kotlinx-coroutines-test. В тестовете можете да замените scope с TestCoroutineDispatcher и ръчно да контролирате изпълнението на корутините.

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

Не, scope е външен контейнер за корутината. Самата корутина не е scope. Въпреки това, внутре в корутина може да се създаде нов scope чрез coroutineScope или supervisorScope за паралелно стартиране на дочерни корутини.

Обобщение

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

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

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също