Чем отличаются launch и async?

В Kotlin корутины можно запускать с помощью двух ключевых функций: launch и async. Обе функции создают корутины внутри CoroutineScope, но имеют разное поведение, предназначение и возвращаемые значения. Их различие принципиально важно для написания корректного асинхронного кода.

Общее между launch и async

  • Обе функции запускают новую корутину.

  • Обе принимают CoroutineContext (например, Dispatchers.IO, SupervisorJob и т.д.).

  • Обе являются функциями-расширениями CoroutineScope.

  • Обе возвращают Job или его производную форму (в случае async — Deferred, который наследуется от Job).

Основное отличие: возвращаемый результат

Функция Возвращает Предназначение
launch Job Запуск задачи без возврата результата (fire-and-forget)
--- --- ---
async Deferred<T> Запуск задачи с возвращением результата (аналог Future/Promise)
--- --- ---

launch: использовать для «побочных эффектов»

launch — это способ запустить корутину, не ожидая от неё значения. Это удобно для операций, где важен сам факт выполнения (или побочный эффект), а не результат.

Пример:

scope.launch {
println("Сохраняем в базу данных...")
repository.saveUser(user)
}
  • Не возвращает никакое значение.

  • Можно отменить через job.cancel().

  • Ошибки внутри launch-корутины могут распространяться вверх по иерархии, если не перехвачены.

async: использовать для получения значения

async — это способ запустить вычисление и получить результат асинхронно. Возвращает Deferred<T>, с которого потом вызывается await() для получения результата.

Пример:

val deferred = scope.async {
fetchUserData()
}
val user = deferred.await()
  • Используется, когда результат корутины нужен позже.

  • Возвращаемый Deferred<T> предоставляет await(), который приостанавливает выполнение до готовности результата.

  • Также можно отменить через deferred.cancel().

  • Ошибки, произошедшие внутри async, проявятся при вызове await().

Пример сравнения

fun CoroutineScope.example() {
// launch
launch {
println("Start launch")
delay(1000)
println("End launch")
}
// async
val deferred = async {
println("Start async")
delay(1000)
println("End async")
return@async 42
}
launch {
val result = deferred.await()
println("Result from async: $result")
}
}
  • launch просто выполняет блок и завершает его.

  • async сохраняет результат, который можно получить через await().

Когда async НЕ нужен

Многие начинающие используют async даже когда не нужен результат. Это считается антипаттерном:

val job = async {
doSomething()
}
//  неправильный подход, лучше launch

В таком случае лучше:

val job = launch {
doSomething()
}

Параллельное выполнение с async

Одно из главных преимуществ async — запуск параллельных задач с последующим await():

val userDeferred = async { fetchUser() }
val postsDeferred = async { fetchPosts() }
val user = userDeferred.await()
val posts = postsDeferred.await()

Такой подход позволяет двум задачам выполняться одновременно, что существенно ускоряет выполнение, особенно при IO-операциях.

Сравнение поведения при ошибках

launch:

scope.launch {
throw Exception("Ошибка в launch") // исключение выбрасывается немедленно
}
  • Ошибка распространяется вверх по иерархии корутин, если не перехвачена.

async:

val deferred = scope.async {
throw Exception("Ошибка в async")
}
val result = deferred.await() // ошибка будет выброшена тут
  • Исключение «отложено» до вызова await().

  • Это удобно, если не нужно обрабатывать ошибку сразу, но может привести к неожиданностям, если await() забыли вызвать.

Особенности Deferred

Deferred<T> — это интерфейс, возвращаемый async, и он имеет дополнительный метод await(). В отличие от Job, который просто указывает на выполнение, Deferred предоставляет результат.

Методы Deferred:

  • await() — получить результат

  • isCompleted — завершено ли выполнение

  • cancel() — отменить выполнение

Пример с обработкой ошибок

val deferred = async {
// Может выбросить исключение
riskyOperation()
}
try {
val result = deferred.await()
println("Успешно: $result")
} catch (e: Exception) {
println("Ошибка при async: ${e.message}")
}

launch внутри async и наоборот

Так можно, но надо быть осторожным.

async {
launch {
println("Это вложенная launch в async")
}
// возвращаем результат
return@async 123
}

Однако launch внутри async не будет управляться Deferred-объектом, а потому его ошибки могут остаться незамеченными. Лучше избегать такого смешения без явной причины.

Использование в Android

viewModelScope.launch {
val user = async { repository.loadUser() }
val posts = async { repository.loadPosts() }
updateUi(user.await(), posts.await())
}

Здесь launch используется как внешний контроллер, а async — для параллельных задач с результатом. Это типичный и правильный подход.

Итоги различий

Характеристика launch async
Возвращает Job Deferred<T>
--- --- ---
Возврат значения Нет Да (T через await())
--- --- ---
Использование Побочные эффекты (UI, логика) Вычисления с результатом
--- --- ---
Обработка ошибок Мгновенно Через await()
--- --- ---
Синтаксис ожидания join() await()
--- --- ---
Может запускаться параллельно Да Да
--- --- ---

Оба подхода тесно связаны с концепцией структурированной конкуренции и должны использоваться внутри CoroutineScope для надёжного контроля жизненного цикла.