async — Coroutine Builder w Kotlinie, który uruchamia współprogram i zwraca obiekt Deferred<T> do uzyskania wyniku operacji asynchronicznej. async umożliwia równoległe wykonywanie wielu zadań i agregowanie ich wyników przez await(). Według przewodnika JetBrains, 2024, async jest preferowanym sposobem organizacji obliczeń równoległych w korutynach.
Najważniejsze
async — to funkcja rozszerzenia CoroutineScope, tworząca współprogram z możliwością zwrócenia wyniku. W przeciwieństwie do launch, async zwraca Deferred<T> — obiekt reprezentujący przyszłą wartość. Współprogram uruchomiony przez async wykonuje się współbieżnie z innymi współprogramami, co umożliwia zrównoleglenie obliczeń.
async jest używany, gdy wynik operacji asynchronicznej jest potrzebny do dalszego przetwarzania: ładowanie danych z wielu źródeł, przetwarzanie wsadowe kolekcji, równoległe zapytania do różnych API. Deferred jest tworzony natychmiast, a wynik staje się dostępny po zakończeniu współprogramu.
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())
}
Sygnatura async jest podobna do launch: te same parametry CoroutineContext, CoroutineStart i suspend-blok. Różnica polega na typie zwracanym — Deferred<T> zamiast Job, gdzie T jest typem wyniku zwróconego przez ostatnie wyrażenie bloku.
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T>
Deferred<T> — interfejs dziedziczący Job i dodający metody await(), getCompleted(), getCompletionExceptionOrNull(). await() jest suspend-funkcją i może być wywołany tylko wewnątrz współprogramu lub innej suspend-funkcji. getCompleted() — metoda nieblokująca, rzucająca wyjątkiem, jeśli współprogram jeszcze się nie zakończył.
Główną zaletą async jest możliwość uruchamiania zadań równolegle. Zamiast sekwencyjnego oczekiwania na każdą operację (całkowity czas = suma czasów), async uruchamia współprogramy jednocześnie, skracając całkowity czas do czasu najdłuższej operacji.
Typowy scenariusz — ładowanie profilu użytkownika, jego ustawień i historii zamówień trzema równoległymi zapytaniami async. Po zakończeniu wszystkich trzech wyniki są łączone w jeden model danych.
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()
)
}
Funkcja coroutineScope uruchamia podrzędne współprogramy i oczekuje ich zakończenia przed zwróceniem wyniku. Cały blok wykonuje się równolegle, a nie sekwencyjnie.
Deferred obsługuje kilka metod pracy z wynikiem. Oprócz await() dostępne są metody sprawdzania statusu, pobierania gotowej wartości i agregacji wielu Deferred za pomocą funkcji narzędziowych Kotlin Coroutines.
| Metoda | Opis | Suspend? |
|---|---|---|
| await() | Wstrzymuje współprogram do uzyskania wyniku | Tak |
| getCompleted() | Zwraca wynik, jeśli współprogram jest zakończony (w przeciwnym razie Exception) | Nie |
| getCompletionExceptionOrNull() | Zwraca wyjątek przy błędzie lub null | Nie |
| isCompleted | Sprawdza, czy współprogram jest zakończony | Nie |
Do agregacji listy Deferred używana jest awaitAll() — funkcja wstrzymująca wykonanie do zakończenia wszystkich Deferred w kolekcji. Jeśli choć jeden zakończył się błędem, awaitAll() rzuca wyjątek.
suspend fun <T> loadAll(requests: List<suspend () -> T>): List<T> {
return coroutineScope {
val deferreds = requests.map { async { it() } }
deferreds.awaitAll()
}
}
Wybór między async a launch zależy od tego, czy potrzebny jest wynik wykonania współprogramu. launch zwraca Job i nadaje się do operacji fire-and-forget, async zwraca Deferred i jest używany do zadań zwracających dane. Pod względem składni i parametrów budownicze są identyczne.
Ważna zasada: never use async for fire-and-forget. Jeśli wynik współprogramu nie jest potrzebny, użyj launch. async tworzy narzut na tworzenie Deferred, który w takim scenariuszu nie jest uzasadniony.
W Androidzie async jest używany do operacji równoległych wewnątrz ViewModel i UseCases. Typowe scenariusze: jednoczesne ładowanie profilu i kanału aktualności, pobieranie pogody z kilku serwisów meteorologicznych, wsadowa synchronizacja danych z serwerem.
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 }
}
}
}
Zaleca się opakowywanie grupy async w coroutineScope lub supervisorScope w celu izolacji błędów. supervisorScope umożliwia kontynuację wykonywania pozostałych współprogramów async nawet przy błędzie w jednym z nich — przydatne w przypadku niekrytycznych operacji.
Często zadawane pytania
Można, ale to bez sensu. async bez await() uruchamia współprogram, ale jego wynik jest tracony. Jeśli wynik nie jest potrzebny, użyj launch — nie tworzy on Deferred i jest wydajniejszy pod względem pamięci i wydajności.
Wyjątek wewnątrz async jest zapisywany w Deferred. Wywołanie await() przerzuca ten wyjątek. Do sprawdzenia bez przerzucania użyj getCompletionExceptionOrNull(). Przy użyciu coroutineScope błąd anuluje wszystkie podrzędne współprogramy.
awaitAll() — funkcja rozszerzenia dla Iterable<Deferred<T>>, wstrzymująca współprogram do zakończenia wszystkich Deferred. Zwraca listę wyników. W przypadku błędu choć jednego Deferred rzuca wyjątek i anuluje pozostałe.
Tak. async — funkcja rozszerzenia CoroutineScope, związana z zakresem. GlobalScope.async tworzy współprogram bez powiązania z cyklem życia — może wykonywać się w nieskończoność. W Androidzie GlobalScope nie jest zalecany ze względu na ryzyko wycieków.
Liczba jest podyktowana dyspozytorem: Dispatchers.IO obsługuje do 64 równoczesnych wątków, Dispatchers.Default — według liczby rdzeni CPU. Dla operacji masowych używaj ograniczonego równoległości przez Semaphore lub mapNotNull z awaitAll.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również