За счет чего горутины 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 и поток второй такой функции.
В целом выглядит все довольно логично, но только я не понимаю почему вывод получился одинаковый? По логике если потоки реально выполнялись параллельно то вывод должен был бы выглядеть как 001122 разве нет?
Правильно ли я понимаю что горутины в 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
