странное ускорение в сопрограммах kotlin?

#kotlin #concurrency

#kotlin #параллелизм

Вопрос:

У меня есть эти два фрагмента кода kotlin для измерения времени запуска потоков.

Первый:

 import kotlinx.coroutines.*
fun main() {
 runBlocking {
     for(i in 0..99){
        val start = System.nanoTime()
           launch {
               val time = System.nanoTime() - start
               println("Starttime: %,d".format(time))
          }
       }
    }
  }
  

Этот код начинается с печати чего-то в диапазоне 35.000.000 и проходит через 100итераций, где значение становится больше и заканчивается около 75.000.000.
Теперь, если я запущу этот код

 import kotlinx.coroutines.*
fun main() {
     runBlocking {
         val list:MutableList<Long> = MutableList<Long>(100, {0})
         for(i in 0..99){
             val start = System.nanoTime()
             launch {
                   list[i] = System.nanoTime() - start
                 }
      }
    delay(1000) // wait for all coroutines to have stored result
    list.forEach{ println("Starttime: %,d".format(it)) }
 }
}
  

Этот код, очевидно, быстрее, поскольку оператор печати не включен в запуск. Но есть и другое странное поведение. Он начинается около 50.000.000, переходит непосредственно к 17.083.000, а затем медленно поднимается обратно до 29.507.300, где и заканчивается.

Итак, мои вопросы: почему первый фрагмент кода так быстро ухудшается и быстро замедляется, в то время как второй код может быть намного быстрее и не замедляется со временем, когда я добавляю больше сопрограмм?

Ответ №1:

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

Я бы предложил изменить ваши примеры кода:

Первый:

 suspend fun main() {
    // dummy calls to preload classes before measuring
    val s = System.nanoTime()
    GlobalScope.launch { println("starting %,d".format(s)) }.join()

    repeat(100) { i ->
        val start = System.nanoTime()
        GlobalScope.launch {
            val time = System.nanoTime() - start
            println("Starttime[$i]: %,d".format(time))
        }
    }
    delay(1000) // wait for all coroutines to print result
}
  

Второй:

 suspend fun main() {
    // dummy calls to preload classes before measuring
    val s = System.nanoTime()
    GlobalScope.launch { println("starting %,d".format(s)) }.join()

    val list: MutableList<Long> = MutableList<Long>(100, { 0 })
    repeat(100) { i ->
        val start = System.nanoTime()
        GlobalScope.launch {
            list[i] = System.nanoTime() - start
        }
    }
    delay(1000) // wait for all coroutines to have stored result
    list.forEachIndexed { i, it -> println("Starttime[$i]: %,d".format(it)) }
}
  

Первый образец, очевидно, «ухудшается», поскольку вы просто мгновенно накапливаете 100 заданий, в то время как для доступа к ним требуется значительное количество времени для System.out выполнения операции печати. Вы также можете столкнуться с тем, что они, скорее всего, не печатают [их индекс] по порядку.

Второй пример более или менее стабилен, потому что задания не выполняют никаких тяжелых операций, поэтому в пуле обычно есть свободный поток Dispatchers.Default для немедленного выполнения блока запуска.

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

1. Спасибо! но когда я не указываю диспетчера, разве все это не выполняется только в основном потоке?

2. @n00bster нет, launch используется Dispatchers.Default по умолчанию. Запуск в основном потоке не имеет смысла, поскольку он просто блокируется.

3. хм, но если я включу печать Thread.currentThread в свою печать, запуск без какого-либо диспетчера будет указывать mainthread, в то время как Dispatchers.default будет чередоваться между потоками?

4. Если вы используете launch его как дочерний runBlocking элемент, он наследует свой контекст сопрограммы, поэтому он действительно будет выполняться блокирующим образом.

5. итак, в моем примере dispatchers.default — это не то же самое, что запуск без явного диспетчера?