За счет чего горутины GO выполняются быстрее?

Читаю статью про горутины где приводится такой код

package main

import (
    "fmt"
    "time"
)

func main() {
  start := time.Now()
  func() {
    for i:=0; i < 3; i++ {
      fmt.Println(i)
    }
  }()

  func() {
    for i:=0; i < 3; i++ {
      fmt.Println(i)
    }
  }()

  elapsedTime := time.Since(start)

  fmt.Println("Total Time For Execution: " + elapsedTime.String())

  time.Sleep(time.Second)
}

И вывод программы

0
1
2
0
1
2

Далее добавляются горутины

package main

import (
    "fmt"
    "time"
)

func main() {
  start := time.Now()
  go func() {
    for i:=0; i < 3; i++ {
      fmt.Println(i)
    }
  }()

  go func() {
    for i:=0; i < 3; i++ {
      fmt.Println(i)
    }
  }()

  elapsedTime := time.Since(start)

  fmt.Println("Total Time For Execution: " + elapsedTime.String())

  time.Sleep(time.Second)
}

И вывод программы

0
1
2
0
1
2

При этом идет ускорение почти в 3 раза. Объясняется что запускается 3 внутренних "потока"

В данном сценарии в конкурентном режиме будут выполняться три потока: основной main, поток первой функции немедленного выполнения first и поток второй такой функции.

  1. В целом выглядит все довольно логично, но только я не понимаю почему вывод получился одинаковый? По логике если потоки реально выполнялись параллельно то вывод должен был бы выглядеть как 001122 разве нет?

  2. Правильно ли я понимаю что горутины в GO это больше похоже на механизм асинхронного программирования т.е. горутины выполняются последовательно но переключаются при любом i\o действии?


Ответы (1 шт):

Автор решения: Алексей Курганский

ускорение в 3 раза с горутинами вы получили именнно потому, что вы перестали замерять время реальной работы, которую вы эмулируете в горутинах. Дело в том что выполнение кода в горутине main не блокируется при вызове горутин. Поэтому значение переменной elapsedTime вычисляется практически сразу, не дожидаясь выполнения запущенных вами горутин. Убедиться в этом можно если вы закомментируете строчку //time.Sleep(time.Second) - в этом случае main выполнится и завершится выполнение всей программы - при этом может быть успеет что-то отработать, а может нет.

Второй момент - гарантированной полученной последовательности на самом деле не будет. Можно для наглядности немного переписать код:

package main

import (
    "fmt"
    "time"
)

func main() {
    start := time.Now()
    go func() {
        for _, i := range []string{"a", "b", "c"} {
            fmt.Println(i)
        }
    }()

    go func() {
        for _, i := range []string{"e", "f", "g"} {
            fmt.Println(i)
        }
    }()

    elapsedTime := time.Since(start)

    fmt.Println("Total Time For Execution: " + elapsedTime.String())

    time.Sleep(time.Second)
}

И его вывод будет постоянно меняться. Например пара запусков:

a
e
Total Time For Execution: 8.459µs
b
c
f
g

Total Time For Execution: 7.75µs
e
a
f
g
b
c

Вывод как раз показывает неверный момент замера времени + разная последовательность.

Чтобы измерить настоящее время исполнения - можно использовать либо каналы, либо sync.WaitGroup{}:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    start := time.Now()

    wg := &sync.WaitGroup{}
    wg.Add(1)
    go func() {
        for _, i := range []string{"a", "b", "c"} {
            fmt.Println(i)
        }
        wg.Done()
    }()

    wg.Add(1)
    go func() {
        for _, i := range []string{"e", "f", "g"} {
            fmt.Println(i)
        }
        wg.Done()
    }()

    wg.Wait()
    fmt.Println("Total Time For Execution: " + time.Since(start).String())
}

e
f
g
a
b
c
Total Time For Execution: 346.583µs

PS: замерять в go таким образом производительность не принято, советую почитать статью на хабре https://habr.com/ru/post/268585/ о бэнчмарках в golang

введите сюда описание изображения

→ Ссылка