Search code examples
androidperformancekotlinkotlin-coroutinesandroid-threading

Is every function that is called inside launch(Dispatchers.IO) also called in the IO Dispatcher?


Currently, I am trying to optimize my app performance by improving the usage of different Dispatchers and contexts. One question I stumbled upon is that If I launch a suspend function inside a coroutine with a IO Dispatcher, will every other function be executed in the same dispatcher as well?

Example

fun doSomething() {
    viewModelScope.launch(Dispatchers.IO) {
       getUserData(viewModelScope)
    }
}

fun getUserData(innerScope: CoroutineScope) {
    workerList.startUserDataWorker()
    observeUserData(innerScope) // suspend function, is this called inside the IO Dipatcher?
}

// Will this be called inside the IO Dispatcher?
private suspend fun observeUserData(innerScope: CoroutineScope) {
    observerWorkerStateAndPassData(workerList.userDataWorkInfo, USER_DATA_OUTPUT_OPTION).collect { status ->
        when(status) {
            is Status.Loading -> {
                _userDataState.postValue(Status.loading())
            }
            is Status.Success -> {
                 // Will getShippingAddressList() also be called on the IO Dispatcher?
                _userDataState.postValue(Status.success(getShippingAddressList()))
            }
            is Status.Failure -> {
                _userDataState.postValue(Status.failed(status.message.toString()))
            }
        }
    }
}

// Getting Address from the local room cache. Is this called on the IO Dispatcher?
private suspend fun getShippingAddressList(): List<UserDeliveryAddress> {
    val uncachedList = userAddressDao.getAllAddress(UserAddressCacheOrder.SHIPPING)
    return userAddressCacheMapper.mapFromEntityList(uncachedList)
}

Solution

  • Aside from the below exceptions, the dispatcher you're using is irrelevant when calling a suspend function. It is only relevant when calling blocking functions. Suspending doesn't use a dispatcher thread.

    Exceptions:

    • Your suspend function is improperly designed and it actually blocks. This breaks convention for coroutines. A suspend function should never block.
    • Maybe your suspend function doesn't block, but it touches objects that are only allowed to be used on a specific thread, for example, most View classes on Android. There's no official convention for this situation--you could skip wrapping the contents of the function with withContext(Dispatchers.Main), and thereby pass along the main-only restriction up from the view class to your suspend function caller. IMO, since you can avoid the potential problem in the first place using withContext, you might as well, at least when the suspend function is public. You can use Dispatchers.Main.immediate to avoid an unnecessary thread switch.
    • Concurrency implications if you're working with objects across multiple simultaneous coroutines. For example, if you only touch a specific object using Main or a single-thread dispatcher, you don't have to worry about multiple threads touching it simultaneously. I would argue for proper encapsulation you should always wrap these usages of the object of concern using withContext(mySingleThreadDispatcher) so it will still be irrelevant which dispatcher is calling your suspend function.

    In your example, it doesn't matter what dispatcher calls observeUserData because the function will suspend indefinitely while it collects. And when it collects, it only calls the non-blocking, thread-safe function LiveData.postValue().