androidkotlinasync-awaitkotlin-coroutines

async await: Calling .await() on Deferred immediately - Correct?


These code is from a Udemy-course, which I'm following currently:

fun getWeatherData() {
        viewModelScope.launch(exceptionHandler) {
            uiState = try {
                val currentWeather = async { getCurrentData() }.await()
                val forecastWeather = async { getForecastData() }.await()

                WeatherHomeUiState.Success(Weather(currentWeather, forecastWeather))
            } catch (e: Exception) {
                Log.e("WeatherHomeViewModel", e.message.toString(),)
                WeatherHomeUiState.Error
            }
        }
    }

Is it correct to call await on the Deferred immediately? Doesn't it lead to sequential execution of the two functions "getCurrentData" and "getForecastData"?

Wouldn't it wait until "getCurrentData" is finished and then start "getForecastData"?

Shouldn't it be instead:

val currentWeather = async { getCurrentData() }
val forecastWeather = async { getForecastData() }
            
WeatherHomeUiState.Success(Weather(currentWeather.await(), forecastWeather.await()))

First trigger both function, so they are running already and then waiting.


Solution

  • Yes, calling await immediately after async is nonsense. It is basically the same as simply writing

    val currentWeather = getCurrentData()
    val forecastWeather = getForecastData()
    

    You gain nothing, but you incur the overhead of launching two coroutines and having overcomplicated code which leaves the reader to wonder why, effectively decreasing the readability of the code.

    If you want the two functions to be executed in parallel you have to call them like you proposed, calling await later:

    val currentWeather = async { getCurrentData() }
    val forecastWeather = async { getForecastData() }
    
    WeatherHomeUiState.Success(Weather(currentWeather.await(), forecastWeather.await()))
    

    Please keep in mind, though, that the code is only allowed to run in parallel now, if it actually is run in parallel depends on how the two functions actually work. Since the viewModelScope uses the Main dispatcher which only has a single thread (the Main thread), the new coroutines that are launched in that scope will also run on that single thread. True parallelism can only be achieved by using multiple threads, though.

    There are two options to mitigate that:

    1. Only if at least one of the two functions switches to another dispatcher (with withContext(Dispatchers.IO) { ... }, for example) their coroutine is moved to another thread where it runs in parallel to the rest.

    2. Even if the two functions stay on the same thread their execution may overlap. Although only at most one coroutine can run on the thread at any given time, if the first coroutine is suspended (which could happen when getCurrentData() waits for some network data to arrive, for example), then the second coroutine can occuppy the thread. Only when that suspends itself (or eventually finishes) the first coroutine continues (still on the same thread). When the majority of the execution time of the two functions consists of waiting, this waiting can be done in parallel. That works because you don't need a thread to wait. You only need it to execute code.

      So, even when no code is actually executed in parallel, you still cut down the overall execution time, allowing other code to run while waiting.