async — Coroutine Builder в Kotlin, който стартира корутина и връща обект Deferred<T> за получаване на резултата от асинхронна операция. async позволява паралелно изпълнение на няколко задачи и агрегиране на резултатите им чрез await(). Според ръководството на JetBrains, 2024, async е предпочитаният начин за организиране на паралелни изчисления в корутини.
Основни точки
async — е разширяваща функция на CoroutineScope, която създава корутина с връщане на резултат. За разлика от launch, async връща Deferred<T> — обект, представляващ бъдеща стойност. Корутината, стартирана чрез async, се изпълнява конкурентно с други корутини, което позволява паралелизиране на изчисленията.
async се използва, когато резултатът от асинхронна операция е необходим за по-нататъшна обработка: зареждане на данни от множество източници, пакетна обработка на колекции, паралелни заявки към различни API-та. Deferred се създава незабавно, а резултатът става достъпен след завършване на корутината.
import kotlinx.coroutines.*
suspend fun String.delayedValue(): String {
delay(1000L)
return this
}
fun main() = runBlocking {
val deferred: Deferred<String> = async { "Hello".delayedValue() }
println(deferred.await())
}
Подписът на async е подобен на launch: същите параметри CoroutineContext, CoroutineStart и suspend-блок. Разликата е в връщания тип — Deferred<T> вместо Job, където T е типът на резултата, върнат от последния израз на блока.
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
Deferred<T> — интерфейс, наследяващ Job и добавящ методите await(), getCompleted(), getCompletionExceptionOrNull(). await() е suspend-функция и може да бъде извикана само вътре в корутина или друга suspend-функция. getCompleted() — неблокиращ метод, който хвърля изключение, ако корутината все още не е завършила.
Основното предимство на async е възможността за паралелно стартиране на задачи. Вместо последователно изчакване на всяка операция (общо време = сума от времената), async стартира корутините едновременно, намалявайки общото време до времето на най-дългата операция.
Типичен сценарий — зареждане на потребителски профил, неговите настройки и история на поръчките с три паралелни async заявки. След завършване на трите, резултатите се комбинират в единен модел данни.
suspend fun loadUserDashboard(userId: String): Dashboard = coroutineScope {
val profile = async { api.getProfile(userId) }
val settings = async { api.getSettings(userId) }
val orders = async { api.getRecentOrders(userId) }
Dashboard(
profile = profile.await(),
settings = settings.await(),
orders = orders.await()
)
}
Функцията coroutineScope стартира дъщерни корутини и изчаква тяхното завършване преди да върне резултата. Целият блок се изпълнява паралелно, а не последователно.
Deferred поддържа няколко метода за работа с резултата. Освен await(), налични са методи за проверка на статуса, получаване на готова стойност и агрегиране на множество Deferred чрез помощни функции на Kotlin Coroutines.
| Метод | Описание | Suspend? |
|---|---|---|
| await() | Спира корутината до получаване на резултата | Да |
| getCompleted() | Връща резултата, ако корутината е завършила (иначе Exception) | Не |
| getCompletionExceptionOrNull() | Връща изключението при грешка или null | Не |
| isCompleted | Проверява дали корутината е завършила | Не |
За агрегиране на списък от Deferred се използва awaitAll() — функция, която спира изпълнението докато всички Deferred в колекцията завършат. Ако поне един е завършил с грешка, awaitAll() хвърля изключение.
suspend fun <T> loadAll(requests: List<suspend () -> T>): List<T> {
return coroutineScope {
val deferreds = requests.map { async { it() } }
deferreds.awaitAll()
}
}
Изборът между async и launch зависи от това дали резултатът от изпълнението на корутината е необходим. launch връща Job и е подходящ за fire-and-forget операции, async връща Deferred и се използва за задачи, връщащи данни. По отношение на синтаксис и параметри, строителите са идентични.
Важно правило: никога не използвайте async за fire-and-forget. Ако резултатът от корутината не е необходим, използвайте launch. async създава допълнително натоварване за създаване на Deferred, което не е оправдано в такъв сценарий.
В Android async се използва за паралелни операции вътре в ViewModel и UseCases. Типични сценарии: едновременно зареждане на профил и новинарски поток, получаване на времето от няколко метеорологични услуги, пакетна синхронизация на данни със сървъра.
class HomeViewModel : ViewModel() {
fun loadHomeScreen() {
viewModelScope.launch {
val userData = async(Dispatchers.IO) { repository.getUserData() }
val newsFeed = async(Dispatchers.IO) { repository.getNewsFeed() }
val notifications = async(Dispatchers.IO) { repository.getNotifications() }
val state = HomeState(
user = userData.await(),
news = newsFeed.await(),
unread = notifications.await()
)
_uiState.update { state }
}
}
}
Препоръчва се групата async да бъде обвита в coroutineScope или supervisorScope за изолация на грешки. supervisorScope позволява продължаване на изпълнението на останалите async корутини дори при грешка в една от тях — полезно за некритични операции.
Често задавани въпроси
Може, но е безсмислено. async без await() стартира корутината, но резултатът ѝ се губи. Ако резултатът не е необходим, използвайте launch — той не създава Deferred и е по-ефективен по отношение на памет и производителност.
Изключението вътре в async се запазва в Deferred. Извикването на await() прехвърля това изключение. За проверка без прехвърляне използвайте getCompletionExceptionOrNull(). При използване на coroutineScope грешката отменя всички дъщерни корутини.
awaitAll() — разширяваща функция за Iterable<Deferred<T>>, която спира корутината докато всички Deferred завършат. Връща списък с резултати. Ако поне един Deferred завърши с грешка, прехвърля изключението и отменя останалите.
Да. async е разширяваща функция на CoroutineScope, обвързана с обхвата. GlobalScope.async създава корутина без обвързване с жизнения цикъл — може да се изпълнява безкрайно. В Android GlobalScope не се препоръчва поради риск от изтичане на памет.
Броят се диктува от диспечера: Dispatchers.IO поддържа до 64 едновременни нишки, Dispatchers.Default — според броя на CPU ядрата. За масови операции използвайте ограничен паралелизъм чрез Semaphore или mapNotNull с awaitAll.
Резюме
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също