Какие варианты использования подходят для Dispatchers.Default в Kotlin? - PullRequest
1 голос
/ 19 июня 2020

Согласно документации, размер пула потоков диспетчеров IO и Default ведет себя следующим образом:

  • Dispatchers.Default: по умолчанию максимальный уровень параллелизма, используемый этим диспетчером, равен к количеству ядер процессора, но не менее двух.
  • Dispatchers.IO: по умолчанию установлено ограничение в 64 потока или количество ядер (в зависимости от того, что больше).

Если я не пропущу хоть одну часть информации, выполнение большого количества операций с интенсивным использованием ЦП на Default более эффективно (быстрее), потому что переключение контекста будет происходить реже .

Но следующий код на самом деле работает намного быстрее на Dispatchers.IO:

fun blockingWork() {
    val startTime = System.currentTimeMillis()
    while (true) {
        Random(System.currentTimeMillis()).nextDouble()
        if (System.currentTimeMillis() - startTime > 1000) {
            return
        }
    }
}

fun main() = runBlocking {
    val startTime = System.nanoTime()
    val jobs = (1..24).map { i ->
        launch(Dispatchers.IO) { // <-- Select dispatcher here
            println("Start #$i in ${Thread.currentThread().name}")
            blockingWork()
            println("Finish #$i in ${Thread.currentThread().name}")
        }
    }
    jobs.forEach { it.join() }
    println("Finished in ${Duration.of(System.nanoTime() - startTime, ChronoUnit.NANOS)}")
}

Я выполняю 24 задания на 8-ядерном ЦП (поэтому я могу держать все потоки диспетчера Default занятыми). Вот результаты на моей машине:

Dispatchers.IO --> Finished in PT1.310262657S
Dispatchers.Default --> Finished in PT3.052800858S

Можете ли вы сказать мне, что мне здесь не хватает? Если IO работает лучше, зачем мне использовать диспетчер, отличный от IO (или любой пул потоков с большим количеством потоков).

1 Ответ

2 голосов
/ 19 июня 2020

Отвечая на ваш вопрос: Default диспетчер лучше всего подходит для задач, в которых нет функции блокировки, потому что нет выигрыша от превышения максимального параллелизма при одновременном выполнении таких рабочих нагрузок ( the-difference-between-concurrent-and-parallel- выполнение ).

https://www.cs.uic.edu/~jbell/CourseNotes/OperatingSystems/5_CPU_Scheduling.html


Ваш эксперимент ошибочен. Как уже упоминалось в комментариях, ваш blockingWork не привязан к ЦП, а к вводу-выводу. Все дело в ожидании - периодах, когда ваша задача заблокирована, и ЦП не может выполнять свои последующие инструкции. Ваш blockingWork, по сути, просто "подождите 1000 миллисекунд", и ожидание 1000 мс X раз параллельно будет быстрее, чем выполнение этого в последовательности. Вы выполняете некоторые вычисления (генерируете случайное число, которое, по сути, также может быть связано с вводом-выводом), но, как уже отмечалось, ваши рабочие генерируют большее или меньшее количество этих чисел, в зависимости от того, сколько времени базовые потоки были переведены в спящий режим.

Я провел несколько простых экспериментов с генерацией чисел Фибоначчи (часто используемых для моделирования рабочих нагрузок процессора). Однако после учета JIT в JVM я не мог легко получить какие-либо результаты, доказывающие, что диспетчер Default работает лучше. Возможно, переключение контекста не так важно, как можно подумать. Возможно, диспетчер не создавал больше потоков с диспетчером ввода-вывода для моей рабочей нагрузки. Возможно, мой эксперимент тоже был ошибочным. Не могу быть уверенным - тестирование производительности на JVM само по себе непросто, и добавление сопрограмм (и их пулов потоков) в микс, конечно, не делает его проще.

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

Ваша программа работает в потоках. Если все потоки заблокированы, ваша программа ничего не делает. Создание новых потоков обходится дорого (в основном с точки зрения памяти), поэтому для высоконагруженных систем блокировка функций становится ограничивающим фактором. Kotlin проделал потрясающую работу по введению функций "приостановки". Параллелизм вашей программы больше не ограничивается количеством имеющихся у вас потоков. Если одному потоку нужно подождать, он просто приостанавливается, а не блокирует поток. Однако «мир не идеален», и не все «приостанавливается» - все еще остаются «блокирующие» вызовы - насколько вы уверены, что используемая no библиотека выполняет такие вызовы под капотом? С большой властью приходит большая ответственность. При использовании сопрограмм нужно быть еще более осторожным в отношении взаимоблокировок, особенно при использовании диспетчера Default. На самом деле, на мой взгляд, диспетчер IO должен быть диспетчером по умолчанию.

...