What is the Android coroutine equivalent to Executor service

Viewed 1947

I have an ExecutorService code snippet, and I am trying to convert it to coroutines in order to test performance. However no matter how I structure the coroutine code, ExecutorService is much faster. I thought that coroutines are supposed to improve performance

functionality:

  • run in background thread
  • execute 200000 actions (counter++)
  • post time that passed to UI thread
  • the code runs in a ViewModel updating a time text view
  • executor service code take ~150 milliseconds on an emulator
  • any coroutines code I wrote takes a lot longer

what would be the coroutine equivalent to the following code :

fun runCodeExecutorService() {
        spinner.value = true
        val executorService = Executors.newFixedThreadPool(NUMBER_OF_CORES * 2)
        val result = AtomicInteger()

        val startTime = System.currentTimeMillis()

        val handler: Handler = object : Handler(Looper.getMainLooper()) {
            override fun handleMessage(inputMessage: Message) {
                time.value = toTime(System.currentTimeMillis() - startTime)
                spinner.value = false
                Log.d("tag", "counter Executor = " + result.get())
            }
        }
        thread(start = true) {
            for (i in 1..NUMBER_OF_THREADS) {
                executorService.execute {
                    result.getAndIncrement()
                }
            }
            executorService.shutdown();
            executorService.awaitTermination(2, TimeUnit.MINUTES)
            val msg: Message = handler.obtainMessage()
            val bundle = Bundle()
            bundle.putInt("MSG_KEY", result.get())
            msg.data = bundle

            handler.sendMessage(msg)
        }
    }

where NUMBER_OF_CORES is val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors() NUMBER_OF_THREADS is 200000

2 Answers

A rough equivalent is dispatchers.

suspend fun aLotOfCoroutines() {
    spinner.value = true
    val result = AtomicInteger()

    val startTime = System.currentTimeMillis()

    coroutineScope {
        repeat(NUMBER_OF_THREADS) {
            launch(Dispatchers.Default) { // 1
                result.getAndIncrement()
            }
        }
    } // 2

    // 3
    time.value = toTime(System.currentTimeMillis() - startTime)
    spinner.value = false
    Log.d("tag", "counter Dispatchers.Default = " + result.get())
}
  1. Instead of creating and stopping a new executor, we can use Dispatchers.Default for non-blocking tasks.

  2. With structured concurrency, coroutineScope does not return until all of its child coroutines are finished. That's why we do not need to explicitly wait for the completion.

  3. Because this method is called in Dispatchers.Main, these line will be also be run in the main thread.


If you really want to use a custom thread pool, you can use the asCoroutineDispatcher extension method on the executor.


I did a bit more investigations after knowing that OP is interested in the performance numbers.

In this "test":

  1. The task is cheap. So any overhead per task is very pronounced.
  2. This task is acting on a resource so heavily contended that it's faster not to parallelize. Running it single-threaded took 1ms.

I think it's fair to say that this is not like any real workload.

Anyway, the following worker pool achieves a similar time with the thread pool.

coroutineScope {
    val channel = produce(capacity = 64) {
        repeat(JOB_COUNT) { send(Unit) }
    }

    // The fewer workers we launch, the faster it runs
    repeat(Runtime.getRuntime().availableProcessors() * 2) {
        launch {
            for (task in channel) {
                result.getAndIncrement()
            }
        }
    }
}

@George actually your code is blocking, the actual code might look something like this:

fun runCodeCoroutines() = viewModelScope.launch {
        spinner.value = true
        val result = AtomicInteger()

        val startTime = System.currentTimeMillis()

        withContext(Dispatchers.Default) {
            result.aLotOfCoroutines()
        }

        // 3
        time.value = toTime(System.currentTimeMillis() - startTime)
        spinner.value = false
        Log.d("tag", "counter Dispatchers.Default = " + result.get())

    }
suspend fun AtomicInteger.aLotOfCoroutines() {

        coroutineScope {
            repeat(NUMBER_OF_THREADS) {
                launch(Dispatchers.Default) { // 1
                    getAndIncrement()
                }
            }
        } // 2

    }

where aLotOfCoroutines, is your code this is approximately the result I got.

benchmark: coroutines code ~ 1.2 seconds executor code ~ 150 milliseconds

there is another version of the code where I break the number of threads into 200*1000

suspend fun massiveRun(action: suspend () -> Unit) {
        coroutineScope { // scope for coroutines
            repeat(NUMBER_OF_TASKS) {
                launch {
                    repeat(NUMBER_OF_ACTIONS) { action() }
                }
            }
        }
    }

which takes ~ 35 - 40 milliseconds however, the same breakdown in Executor service takes ~ 25 - 35 milliseconds

which is closer but still overall better

my conclusion is that when looking at performance Executor Service is still more performant than Coroutines

and coroutines are only better in syntax and when precise performance is not important (i.e network calls etc.)

Related