Какая сопрограмма Android эквивалентна службе исполнителя

#android #kotlin #kotlin-coroutines #coroutine

#Android #kotlin #kotlin-сопрограммы #сопрограмма

Вопрос:

У меня есть ExecutorService фрагмент кода, и я пытаюсь преобразовать его в сопрограммы, чтобы протестировать производительность. Однако независимо от того, как я структурирую код сопрограммы, ExecutorService это намного быстрее. Я думал, что сопрограммы должны повышать производительность

функциональность:

  • запуск в фоновом потоке
  • выполнить 200000 действий (счетчик )
  • время публикации, переданное потоку пользовательского интерфейса
  • код выполняется в ViewModel, обновляя текстовое представление time
  • код службы исполнителя в эмуляторе занимает ~ 150 миллисекунд
  • любой написанный мной код сопрограммы занимает намного больше времени

какая сопрограмма была бы эквивалентна следующему коду :

 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)
        }
    }
  

где NUMBER_OF_CORES — это val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors()
Число потоков равно 200000

Ответ №1:

@George на самом деле ваш код блокируется, фактический код может выглядеть примерно так:

 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

    }
  

где aLotOfCoroutines, ваш код
это примерно тот результат, который я получил.

бенчмарк: код сопрограммы ~ 1,2 секунды, код исполнителя ~ 150 миллисекунд.

существует другая версия кода, в которой я разбиваю количество потоков на 200 * 1000

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

которая занимает ~ 35-40 миллисекунд
однако такая же разбивка в службе исполнителя занимает ~ 25-35 миллисекунд

которая ближе, но все же в целом лучше

мой вывод заключается в том, что при взгляде на производительность Служба Executor по-прежнему более производительна, чем сопрограммы

и сопрограммы лучше только в синтаксисе, и когда точная производительность не важна (например, сетевые вызовы и т.д.)

Комментарии:

1. Я воспринял ваш вопрос как «как написать порождение задачи в идиоматических сопрограммах Kotlin» (эквивалентно семантически). И оказывается, вы пишете бенчмарк. Сопрограмма, конечно, тяжелее, чем Runnable . Она может приостанавливаться и возобновляться без блокировки потока; она управляет отменой и таймаутом. Это сравнение яблок с апельсинами.

2. спасибо за разъяснение, я думал, мне ясно, что в моем вопросе время имеет существенное значение. Сопрограммы документируются как облегченные потоки, и эта официальная ссылка: developer.android.com/kotlin/coroutines-adv предполагаю, что сопрограммы повышают производительность (т. Е. улучшают текущую ситуацию, когда большинство приложений будут иметь механизм исполнителя), тогда как из теста я понимаю, что это не обязательно улучшает его, это просто чище и проще в использовании. есть ли способ с точки зрения производительности, чтобы сопрограммы были лучше, чем исполнители?

3. Они имеют меньший вес, чем потоки. Если ваш сетевой вызов блокирует поток, вы должны ожидать увеличения производительности за счет переключения на обратные вызовы. Поскольку обратными вызовами сложно управлять, сопрограмма является абстракцией для их обработки. Я думаю, отсюда и возникает требование к производительности.

4. Executor Может использоваться в качестве диспетчера сопрограммы. Это говорит вам о том, что сопрограмма представляет собой более высокий уровень абстракции по сравнению с обратными вызовами. Вы можете рассматривать код между возобновлением и приостановкой сопрограммы как Runnable для запуска исполнителем.

Ответ №2:

Грубый эквивалент — диспетчеры.

 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. Вместо создания и остановки нового исполнителя мы можем использовать Dispatchers.Default для неблокирующих задач.

  2. При структурированном параллелизме coroutineScope не возвращается до тех пор, пока не будут завершены все его дочерние сопрограммы. Вот почему нам не нужно явно ждать завершения.

  3. Поскольку этот метод вызывается в Dispatchers.Main , эти строки также будут выполняться в главном потоке.


Если вы действительно хотите использовать пользовательский пул потоков, вы можете использовать asCoroutineDispatcher метод расширения в исполнителе.


Я провел еще немного исследований, узнав, что OP интересуется показателями производительности.

В этом «тесте»:

  1. Задача дешевая. Таким образом, любые накладные расходы на задачу очень заметны.
  2. Эта задача воздействует на ресурс с такой интенсивной нагрузкой, что его быстрее не распараллеливать. Ее однопоточный запуск занял 1 мс.

Я думаю, справедливо будет сказать, что это не похоже ни на какую реальную рабочую нагрузку.

В любом случае, следующий рабочий пул достигает аналогичного времени с пулом потоков.

 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()
            }
        }
    }
}