Люблю Go, люблю его хвалить (бывает даже, привираю слега), люблю о нем статьи. Прочитал статью “Go: Два года в продакшне ”, потом комменты. Стало понятно, на хабре — оптимисты! Хотят верить в лучшее.
По умолчанию Go работает на одном потоке, используя свой шедулер и асинхронные вызовы. (У программиста создается ощущение многопоточности и параллельности.) В этом случае каналы работаю очень быстро. Но если указать Go использовать 2 и больше потока, то Go начинает использовать блокировки и производительность каналов может падать. Не хочется себя ограничивать в использовании каналов. Тем более, большинство сторонних библиотек при каждом удобном случае используют каналы. Поэтому часто эффективно запускать Go с одним потоком, как это сделано по умолчанию.
channel01.go
package main
import "fmt"
import "time"
import "runtime"
type Mes struct{
i int
}
func main() {
numcpu := runtime.NumCPU()
fmt.Println("NumCPU", numcpu)
//runtime.GOMAXPROCS(numcpu)
runtime.GOMAXPROCS(1)
ch1 := make(chan int)
ch2 := make(chan float64)
go func() {
for i := 0; i < 1000000; i++ {
ch1 <- i
}
ch1 <- -1
ch2 <- 0.0
}()
go func() {
total := 0.0
for {
t1 := time.Now().UnixNano()
for i := 0; i < 100000; i++ {
m := <-ch1
if m == -1 {
ch2 <- total
}
}
t2 := time.Now().UnixNano()
dt := float64(t2 - t1) / 1000000.0
total += dt
fmt.Println(dt)
}
}()
fmt.Println("Total:", <-ch2, <-ch2)
}
users-iMac:channel user$ go run channel01.go
NumCPU 4
23.901
24.189
23.957
24.072
24.001
23.807
24.039
23.854
23.798
24.1
Total: 239.718 0
теперь давайте активируем все ядра, перекомментировав строки.
runtime.GOMAXPROCS(numcpu)
//runtime.GOMAXPROCS(1)
users-iMac:channel user$ go run channel01.go
NumCPU 4
543.092
534.985
535.799
533.039
538.806
533.315
536.501
533.261
537.73
532.585
Total: 5359.113 0
20 раз медленней? В чем подвох? Размер канала по умолчанию 1.
ch1 := make(chan int)
Поставим 100.
ch1 := make(chan int, 100)
результат 1 поток
users-iMac:channel user$ go run channel01.go
NumCPU 4
9.704
9.618
9.178
9.84
9.869
9.461
9.802
9.743
9.877
9.756
Total: 0 96.848
результат 4 потока
users-iMac:channel user$ go run channel01.go
NumCPU 4
17.046
17.046
16.71
16.315
16.542
16.643
17.69
16.387
17.162
15.232
Total: 0 166.77300000000002
Всего в два раза медленней, но не всегда можно это использовать.
Пример “канал каналов”
package main
import "fmt"
import "time"
import "runtime"
type Mes struct{
ch chan int
}
func main() {
numcpu := runtime.NumCPU()
fmt.Println("NumCPU", numcpu)
//runtime.GOMAXPROCS(numcpu)
runtime.GOMAXPROCS(1)
ch1 := make(chan chan int, 100)
ch2 := make(chan float64, 1)
go func() {
t1 := time.Now().UnixNano()
for i := 0; i < 1000000; i++ {
ch := make(chan int, 100)
ch1 <- ch
<- ch
}
t2 := time.Now().UnixNano()
dt := float64(t2 - t1) / 1000000.0
fmt.Println(dt)
ch2 <- 0.0
}()
go func() {
for i := 0; i < 1000000; i++ {
ch := <-ch1
ch <- i
}
ch2 <- 0.0
}()
<-ch2
<-ch2
}
результат 1 поток
users-iMac:channel user$ go run channel03.go
NumCPU 4
1041.489
результат 4 потока
users-iMac:channel user$ go run channel03.go
NumCPU 4
11170.616
Поэтому, если у вас 8 ядер и вы пишите сервер на Go, вам не стоит полностью полагаться на Go в распараллеливании программы, а может, запустить 8 однопоточных процессов, а перед ними балансировщик, который тоже можно написать на Go. У нас в продакшине был сервер, который при переходе с одно-ядерного сервера на 4х стал обрабатывать на 10% меньше запросов.
Что значат эти цифры? Перед нами стояла задача обрабатывать 3000 запросов в секунду в одном контексте (например, выдавать каждому запросу последовательно числа: 1, 2, 3, 4, 5… может, чуть сложней) и производительность 3000 запросов в секунду ограничивается в первую очередь каналами. С добавлением потоков и ядер производительность растет не так рьяно, как хотелось. 3000 запросов в секунду для Go — это некий предел на современном оборудовании.
Автор: pyra