Scope можно в чем-то сравнить с Активити Андроида. Если весь ЮИ отрисовывается в рамках Активити и нигде больше, то также и с Корутинами. Их можно запустить в рамках Scope-а. Также и suspend-функции запускаются в рамках scope-а. По примеру борща, scope можно сравнить с тарелкой, в котором будет борщ=)

Scope который отменяется за собой отменяет дочерние Корутины.

myScope.launch {
    // coroutine code
}

Используя билдер Launch прямо из scope-а можно запускать корутины в множественном числе. А после, если нужно будет удалить, то применить метод:

myScope.cancel()

По сути билдеры корутин Launch и Await являются расширениями класса CoroutineScope. То есть вне scope-а корутина не запустится.

Давайте посмотрим, что такое scope «под капотом».

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Это интерфейс. Объект, реализующий этот интерфейс, содержит CoroutineContext (он же контекст). Мы пока не изучали, что это. Если вкратце — то это хранилище, типа Map, в котором могут храниться различные объекты. О контексте поговорим позже.

Обратите внимание на поле coroutineContext. По контракту контекст scope-а должен хранить в себе Jobscope-а. Job является производной от класса CoroutineContext. Это действительно важно и вот почему.

Когда мы создаем корутину, у нее есть Job. Но у Job-а корутины должен быть Job-родитель. Это реализация structured concurrency. Смысл в том, что если мы отменим Job-родитель, то автоматически отменится и Job дочерней корутины.

Возникает вопрос — где взять Job-родитель, чтобы создать корутину. Вот именно для этого и нужен scope. Он (в своем контексте) содержит Job. Этот Job будет являться родителем для Job-ов корутин, которые мы создаем, вызывая myScope.launch().

И когда мы вызываем scope.cancel(), мы отменяем Job-родитель, а это отменит все Job-ы корутин, созданных с помощью этого scope. Т.е. отменой scope, отменяем все его корутины. Это и есть основной смысл scope.

Откуда взять scope?
Scope может быть предоставлен некоторыми объектами с жизненным циклом. Если мы используем этот scope, чтобы запускать корутины, то все его корутины будут отменены, когда объект завершает свою работу.

Пример такого объекта — ViewModel из Architecture Components. У него есть свойство viewModelScope.

class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
        }
    }
}

Корутины, запущенные в этом scope, будут отменены, когда завершится ViewModel.

Каждая корутина создает внутри себя свой scope, чтобы иметь возможность запускать дочерние корутины. Именно этот scope и доступен нам как this в блоке кода корутины.

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

SupervisorScope
Если coroutineScope принимает ошибки от своих дочерних корутин и просто не шлет их дальше в родительскую корутину, то supervisorScope даже не принимает ошибку от дочерних. Это приводит к отличиям в обработке ошибок.

Корутина 1_2, которая пытается передать ошибку наверх в ScopeCoroutine, получает отрицательный ответ и пытается обработать ошибку сама. Для этого она использует предоставленный ей CoroutineExceptionHandler. Если же его нет, то будет крэш, который не поймать никаким try-catch.

Поэтому имеет смысл использовать CoroutineExceptionHandler внутри supervisorScope. В этом случае ошибка попадет в этот обработчик и на этом все закончится. Функция supervisorScope не выбросит исключение и не отменит остальные корутины внутри себя (т.е. корутину 1_1 в примере выше).