Go: многопоточность и параллельность

в 19:08, , рубрики: benchmark, golang, метки: ,

Люблю 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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js