В чем разница между launch/join и async/await в корутинах Kotlin?
Ключевое отличие: возвращаемое значение
Главное различие между launch и async в Kotlin Coroutines — это то, что они возвращают. launch возвращает Job (не имеет результата), а async возвращает Deferred<T> (обещает результат типа T). Это фундаментально меняет подход к работе с корутинами.
Когда использовать launch/join?
launch предназначен для "fire-and-forget" сценариев — когда вам нужно выполнить действие, но не требуется получать от него значение. Примеры:
- Отправка логов на сервер
- Обновление UI (например, анимация)
- Запись данных в файл без ожидания подтверждения
- Параллельное выполнение задач, где результат — побочный эффект
Ваш пример с launch и мутабельными переменными — это антипаттерн. Он работает, но нарушает принцип иммутабельности и может привести к проблемам с потоками. join() просто ждёт завершения корутины, но не возвращает данные.
// Правильное использование launch
fun saveLog(context: CoroutineScope) {
context.launch {
delay(100) // имитация записи
println("Лог сохранён")
}
// Мы не ждём результат, просто запустили
}
Когда использовать async/await?
async создан для случаев, когда корутина должна вернуть значение. Deferred<T> — это легковесный аналог Future или Promise. Вы можете:
- Параллельно выполнять несколько задач и собирать их результаты
- Обрабатывать ошибки каждой задачи отдельно
- Строить цепочки зависимостей
// Пример с async/await
suspend fun fetchUser(id: Int): User = async {
delay(1000)
User(id, "Name")
}.await()
// Параллельный запуск
suspend fun loadDashboard(): Dashboard = coroutineScope {
val user = async { fetchUser(1) }
val posts = async { fetchPosts() }
val stats = async { fetchStats() }
Dashboard(
user = user.await(),
posts = posts.await(),
stats = stats.await()
)
}
Дополнительные различия
- Обработка исключений:
launchвыбрасывает исключение сразу (если не перехвачено), аasync"заворачивает" его вDeferred— вы получите исключение только при вызовеawait(). - Структурированная конкурентность:
asyncвнутриcoroutineScopeилиsupervisorScopeдаёт больше контроля над отменой и ошибками. - Производительность: Оба варианта имеют схожую производительность, но
asyncсоздаёт объектDeferred, что может быть чуть дороже для очень большого количества корутин.
Практический совет
Используйте launch для задач без результата (логи, уведомления, обновление кэша). Используйте async когда вам нужно вернуть значение. В вашем примере с загрузкой данных async/await — правильный выбор, так как он чище, безопаснее и следует функциональному стилю.
// Итоговое сравнение
// launch — для действий (выполнить и забыть)
// async — для вычислений (выполнить и получить результат)
<<Дополнение: разница в обработке ошибок и отмене
Есть ещё один важный аспект, который часто упускают — как эти два подхода ведут себя при возникновении исключений.
С launch исключение выбрасывается немедленно в момент возникновения. Если вы не обернули корутину в try-catch, оно "всплывёт" и может крашнуть приложение (или, в случае runBlocking, вызовет исключение сразу).
С async исключение "откладывается" до момента вызова await(). Это даёт возможность:
- Запустить несколько корутин параллельно и обработать ошибки каждой отдельно
- Использовать
try-catchвокругawait()для точечной обработки - Применять
CoroutineExceptionHandlerдля глобальной обработки
// Пример обработки ошибок с async
val result = try {
async { riskyOperation() }.await()
} catch (e: Exception) {
"Fallback value"
}
Также стоит упомянуть, что async внутри supervisorScope позволяет одной корутине упасть, не отменяя остальные. С launch в обычном coroutineScope ошибка одной корутины отменит все дочерние. Выбирайте подход в зависимости от того, нужно ли вам изолировать ошибки.