Представьте: вы пишете функцию, которая должна загрузить профиль и ленту параллельно, а потом вернуть собранный экран. Пишете так:
suspend fun loadScreen(): Screen {
val scope = CoroutineScope(Dispatchers.IO)
val user = scope.async { api.getUser() }
val feed = scope.async { api.getFeed() }
return Screen(user.await(), feed.await())
}
Работает. Потом вы меняете одну букву — заглавную C на строчную:
suspend fun loadScreen(): Screen = coroutineScope {
val user = async { api.getUser() }
val feed = async { api.getFeed() }
Screen(user.await(), feed.await())
}
Тоже работает. Выглядят почти одинаково, называются почти одинаково — и делают принципиально разное. Первый вариант в проекте оставлять нельзя, второй можно.
Что такое скоуп
Скоуп — контейнер, в котором живёт корутина. Он нужен, чтобы управлять её жизнью: пока скоуп жив, живут запущенные в нём корутины; отменили скоуп — отменились все разом.
Скоупы выстраиваются в дерево. У родителя есть потомки, и родитель не завершится, пока не завершатся они. Отмена родителя отменяет детей. Это и называется структурированной конкурентностью: время жизни задач повторяет структуру кода, а не живёт своей жизнью.
Отсюда и вся разница между двумя функциями.
CoroutineScope() — фабрика
CoroutineScope(context) — обычная функция. Она создаёт новый объект скоупа и возвращает его вам. Всё. Она ничего не запускает, ничего не ждёт и ни к чему не привязана.
Корутины, запущенные в таком скоупе, работают сами по себе. Функция, которая его создала, спокойно завершится, не дожидаясь их. Если никто не позаботится отменить этот скоуп — никто и не отменит.
Поэтому место CoroutineScope() — там, где у объекта есть собственный жизненный цикл и вы знаете, в какой момент его оборвать. Так устроены viewModelScope и lifecycleScope: они созданы ровно этой функцией и отменяются, когда умирает ViewModel или экран.
А вот внутри suspend-функции создавать скоуп вручную — почти всегда ошибка. Вы создали дерево, которое ни к чему не прикреплено, и забыли про него.
coroutineScope — приостановка
coroutineScope { ... } — suspend-функция. Она создаёт скоуп неявно, выполняет в нём ваш блок и приостанавливается, пока не завершатся все запущенные внутри корутины. Только потом возвращает результат.
Вот что это даёт на практике:
- функция не вернёт управление, пока работа не доделана;
- если одна из корутин упала, остальные отменяются, а исключение вылетает наружу — и его можно поймать обычным
try/catchвокруг блока; - если кто-то отменит вызывающую корутину, отменится и всё содержимое блока.
То есть снаружи такая функция выглядит как обычная синхронная: вызвали, дождались, получили результат. А внутри — конкурентность.
| CoroutineScope(context) | coroutineScope { } | |
|---|---|---|
| Тип | обычная функция | suspend-функция |
| Что возвращает | объект скоупа | результат своего блока |
| Ждёт свои корутины | нет | да |
| Связана с родителем | нет | да |
Почему внутри runBlocking можно писать launch без точки
Короткое наблюдение, которое снимает половину путаницы. launch и async объявлены как функции-расширения для CoroutineScope — значит, вызывать их полагается через точку, и скоуп обязан откуда-то взяться.
Но внутри runBlocking мы пишем просто launch { }. Дело в сигнатуре: блок runBlocking объявлен как лямбда с приёмником CoroutineScope. Внутри такой лямбды this — это скоуп, и точку можно опустить. Мы всё это время писали this.launch, просто не видели этого.
И отдельно — GlobalScope
Раз уж речь о скоупах: GlobalScope — это скоуп без родителя, исключённый из иерархии. Его нельзя отменить, можно только дождаться, пока корутины закончатся сами.
В Android это прямая утечка: процесс приложения может висеть в памяти и после закрытия всех экранов — вместе с корутиной, которая всё ещё что-то делает. Компилятор не зря требует отдельной аннотации, чтобы вы им воспользовались.
Если GlobalScope кажется решением — почти наверняка нужен viewModelScope.
Короткое правило
Нужен скоуп, живущий вместе с объектом, — CoroutineScope(), и не забудьте его отменить. Нужно запустить несколько задач и дождаться их прямо здесь — coroutineScope. Внутри suspend-функции в девяти случаях из десяти правильный ответ — второй.
Структурированная конкурентность — одна из глав большого руководства «Корутины в Kotlin».