3

Retrofit 2.6.0 brings us support for the suspend function. Call and enqueue are used under the hood:

Behind the scenes this behaves as if defined as fun user(...): Call and then invoked with Call.enqueue. You can also return Response for access to the response metadata.

This means that request is asynchronous and network call is being done on the ThreadPoolExecutor form OkHttp. We don't care about switching threads as described in this question.

interface ApiService {
    @GET("custom/data/api")
    suspend fun getData(): String
}

class CustomViewModel: ViewMode() {
    // I omitted the initialization of the repository or usecase for simplicity
    fun getData() {
        viewModelScope.launch { // Dispatchers.Main
            apiService.getData() // network call on ThreadPoolExecutor
            // continue on Main thread
        }
    }
}

At this point we have one thread context switching.

But what if I want to do some additional work after network call, for example mapping. And I want to do it not on the main thread:

fun getData() {
    viewModelScope.launch { // Dispatchers.Main
        val result = apiService.getData() // network call on ThreadPoolExecutor
        // continue on Main thread
        val uiData = withContext(Dispatchers.IO) { // Coroutine runs on a shared thread pool
            mapResult(result) // suspending long running task
        }
        // continue on Main thread
    }
}

At this point we have two thread context switching: one for a network cal, the other for mapping.

And my question is about optimization. Is it more optimized to not use the suspend function in the Retrofit interface and use one thread switching with coroutine dispatcher on which the network call and other work will run?

interface ApiService {
    @GET("custom/data/api")
    fun getData(): Call<String>
}

fun getData() {
    viewModelScope.launch { // Dispatchers.Main
        // Main thread
        val uiData = withContext(Dispatchers.IO) { // Coroutine runs on a shared thread pool
            val result = apiService.getData().execute().body() // network call
            mapResult(result) // suspending long running task
        }
        // continue on Main thread
    }
}

I know that in a simple application the optimization is not that big and is measured in nanoseconds, but that is not the main question. Also, the question is not about the code, exceptions handling etc. The question is about understanding the internal mechanism of multithreading with Retrofit suspend support and coroutines.

Sergio
  • 27,326
  • 8
  • 128
  • 149
bitvale
  • 1,959
  • 1
  • 20
  • 27
  • What is `mapResult()`? Is it a CPU-intensive operation or another IO? – broot Dec 19 '21 at 18:29
  • And if it's IO then is it suspending as you said or blocking? – broot Dec 19 '21 at 18:50
  • Does it matter if it CPU-intensive operation or another IO? Or if it is another IO then it can reuse the `ThreadPoolExecutor` form OkHttp? – bitvale Dec 19 '21 at 19:06
  • 1
    It does matter, because optimally we should not run CPU-intensive operations with `Dispatchers.IO`. There is another dispatcher `Dispatchers.Default` and it is optimized for CPU tasks. So we should switch the dispatcher anyway. I think the only thing that is wasteful here is that we switch from IO to main just to immediately switch to CPU/`Default`. At least this is what I believe happens here. It would be better to switch from IO to CPU directly. – broot Dec 19 '21 at 19:36
  • Thank You for clarification about immediately switch. But if it is IO, for example several retrofit calls, is it theoretically more optimized to use coroutine `Dispatchers.IO` instead of `ThreadPoolExecutor` from OkHttp? – bitvale Dec 19 '21 at 20:20
  • I'm not very confident about this, but my guess would be that you're right. Several `execute()` inside `Dispatchers.IO` won't switch threads. Suspend retrofit functions will probably jump between coroutine threads and okhttp threads. Still, I think this isn't a reason to prefer blocking over suspend. – broot Dec 19 '21 at 20:38

1 Answers1

1

Context switching between coroutines is far less expensive than context switching between threads if the coroutines run in the same thread. However, if they are in different threads, switching coroutine contexts requires an expensive thread context switch. Consider next example:

suspend fun doWork() {
    val data1 = fetchData1() // calling suspend function
    val data2 = fetchData2(data1) // calling another suspend function

    withContext(Dispatchers.Main) {
        updateUI(data2)
    }
} 

Here switching coroutine context requires an expensive thread context switch.

In your case the Api call happens in background thread, and you use Dispatchers.IO to run mapping in background thread. So there is a possibility that the mapping will happen in the same background thread. So the operation is not expensive in your case.

But if OkHttp has it's own ExecutorService, to make sure coroutines and OkHttp can use the same Threads we can create common ExecutorService and use it for OkHttp (set it when constructing OkHttpClient with OkHttpClient.Builder.dispatcher) and for coroutines (set it when switching contexts withContext(commomExecutorInstance.asCoroutineDispatcher())).

Sergio
  • 27,326
  • 8
  • 128
  • 149
  • Is it possible that threads from `ThreadPoolExecutor` created in `OkHttp` can be reused by coroutines? – bitvale Dec 22 '21 at 16:19
  • Could you please point to the documentation regarding using `ThreadPoolExecutor` in `OkHttp`. If OkHttp creates its own threads, then I guess reusing those threads by coroutines is not possible. – Sergio Dec 22 '21 at 16:41
  • We can run network request with `Retrofit` in two ways, async (`enqueue`) and sync (`execute`). If we run async, `Retrofit` guarantees (for Android) that the request will be executed on a background thread. That background thread is created by `OkHttp` (as I understood from the source code) [link](https://github.com/square/okhttp/blob/66a076a1dbbc9bffeaafd45bbdefb275f0fbfbd7/okhttp/src/jvmMain/kotlin/okhttp3/Dispatcher.kt?_pjax=%23js-repo-pjax-container%2C%20div%5Bitemtype%3D%22http%3A%2F%2Fschema.org%2FSoftwareSourceCode%22%5D%20main%2C%20%5Bdata-pjax-container%5D#L95) – bitvale Dec 22 '21 at 17:08
  • If that's so, to make sure coroutines and OkHttp can use the same `Thread`s we can create common `ExecutorService` and use it when constructing `OkHttpClient` with `OkHttpClient.Builder.dispatcher` and in coroutines when switching contexts `withContext(commomExecutorInstance.asCoroutineDispatcher())` – Sergio Dec 22 '21 at 17:32