Чем отличаются 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 для надёжного контроля жизненного цикла.