Kotlin CoroutinesMiddleSystem design

Что такое GlobalScope и почему его следует избегать?

GlobalScope — синглтон-scope, живущий всё время работы приложения. Его следует избегать, поскольку он нарушает structured concurrency: корутины не отменяются при уничтожении компонента, провоцируют утечки и делают тесты недетерминированными. Используйте viewModelScope, lifecycleScope или собственный CoroutineScope.

GlobalScope и его проблемы

GlobalScope — это синглтон CoroutineScope, привязанный к жизни всего приложения. Корутины, запущенные в нём, живут, пока живёт JVM/приложение, и их нельзя отменить структурированно. Именно поэтому GlobalScope помечен аннотацией @DelicateCoroutinesApi — его использование требует явного осознанного решения.

Почему GlobalScope опасен

  • Нарушение structured concurrency: корутина в GlobalScope не является дочерней ни для какого родительского scope. Если вы закрываете экран, ViewModel, сервис — корутина продолжает жить.
  • Утечки ресурсов: сетевые соединения, держатели блокировок, ссылки на Activity/Fragment не освобождаются, пока корутина не завершится сама.
  • Невозможность тестирования: тест не может получить Job корутины из GlobalScope, дождаться её завершения или отменить её детерминированно.
  • Неявный диспетчер: по умолчанию — Dispatchers.Default, что может быть неожиданным.

Пример проблемы

class MyViewModel : ViewModel() {
    fun loadData() {
        // ПЛОХО: корутина переживёт уничтожение ViewModel
        GlobalScope.launch {
            val data = repository.fetch()
            _state.value = data  // NPE или crash после onCleared()
        }
    }
}

Правильная альтернатива — viewModelScope

class MyViewModel : ViewModel() {
    fun loadData() {
        // ХОРОШО: корутина отменяется при onCleared()
        viewModelScope.launch {
            val data = repository.fetch()
            _state.value = data
        }
    }
}

Когда GlobalScope всё же допустим

Есть редкие случаи, когда GlobalScope оправдан:

  • Корутина действительно должна жить всё время работы приложения (фоновый daemon-процесс, top-level сервис мониторинга).
  • Код в main() серверного приложения без собственного lifecycle-scope.
  • Явно одноразовые fire-and-forget операции, потеря которых не критична.

Альтернативы для разных контекстов

// Android ViewModel
viewModelScope.launch { /* отменяется с ViewModel */ }

// Android Fragment/Activity
lifecycleScope.launch { /* отменяется с lifecycle */ }
lifecycleScope.launchWhenStarted { /* приостанавливается в STOPPED */ }

// Ktor / серверный код — собственный scope
val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
appScope.launch { /* управляемый scope */ }

// Тесты
TestScope().launch { /* детерминированный clock */ }

// Самостоятельный scope с явной отменой
class MyService {
    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    fun start() { scope.launch { doWork() } }
    fun stop()  { scope.cancel() }
}

Как компилятор предупреждает

@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch { /* явно опт-ин */ }

Без @OptIn IDE покажет предупреждение. Наличие @OptIn в коде — сигнал для ревьюера: «автор знал, что делает».

Подводные камни

  • Утечка Activity: захват this (Activity/Fragment) в лямбде GlobalScope.launch → OutOfMemory после поворота экрана.
  • Тихий сбой после отмены контекста: обновление UI-элементов после их уничтожения приведёт к краш-репортам в продакшне.
  • Скрытые зависимости: unit-тест завершается, но корутина GlobalScope ещё работает — тест проходит, а побочный эффект случается после assert.
  • Диспетчер по умолчанию: GlobalScope использует EmptyCoroutineContext, что означает Dispatchers.Default. IO-задачи там займут thread pool, предназначенный для вычислений.
  • Цепочка отмены рвётся: даже если родительский Job отменён, дочерняя GlobalScope-корутина этого не «почувствует» — она не является дочерней.
  • Тестирование с runTest: runTest (kotlinx-coroutines-test) контролирует только корутины внутри TestScope. GlobalScope-корутины выполняются вне его — тесты становятся недетерминированными.

Common mistakes

  • Объяснять «GlobalScope» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: В обычном use case GlobalScope приводит к утечкам, потерянным исключениям и операциям после отмены запроса.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «GlobalScope» своими словами и связывает ее с кодом.
  • Называет механизм: Он допустим только для явно process-wide задач с собственным error handling и shutdown strategy.
  • Видит production-последствие: В обычном use case GlobalScope приводит к утечкам, потерянным исключениям и операциям после отмены запроса.

Sources

Related topics