Пользователь
0,0
рейтинг
27 сентября 2013 в 23:08

Разработка → Go: многопоточность и параллельность

Go*
Люблю Go, люблю его хвалить (бывает даже, привираю слега), люблю о нем статьи. Прочитал статью “Go: Два года в продакшне”, потом комменты. Стало понятно, на хабре — оптимисты! Хотят верить в лучшее.

По умолчанию Go работает на одном потоке, используя свой шедулер и асинхронные вызовы. (У программиста создается ощущение многопоточности и параллельности.) В этом случае каналы работаю очень быстро. Но если указать Go использовать 2 и больше потока, то Go начинает использовать блокировки и производительность каналов может падать. Не хочется себя ограничивать в использовании каналов. Тем более, большинство сторонних библиотек при каждом удобном случае используют каналы. Поэтому часто эффективно запускать Go с одним потоком, как это сделано по умолчанию.

channel01.go
package main

import "fmt"
import "time"
import "runtime"

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"

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 — это некий предел на современном оборудовании.

Ночной Update: Как нельзя оптимизировать



Комменты из статьи “Go: Два года в продакшне” побудили меня написать эту статью, но комменты этой превзошли комменты первой.

Хабражитель cybergrind предложил следующую оптимизацию. Она очень понравилась уже 8 другим хабражителям. Не знаю, читали они код или может они дайверы и все делают интуитивно, но я поясню. Так статья станет более полной и информативной.
Вот код:

package main
 
import "fmt"
import "time"
import "runtime"
 
 
func main() {
 
    numcpu := runtime.NumCPU()
    fmt.Println("NumCPU", numcpu)
    //runtime.GOMAXPROCS(numcpu)
    runtime.GOMAXPROCS(1)
 
    ch3 := make(chan int)
    ch1 := make(chan int, 1000000)
    ch2 := make(chan float64)
 
 
    go func() {
 
        for i := 0; i < 1000000; i++ {
            ch1 <- i
        }
        ch3 <- 1
        ch1 <- -1
        ch2 <- 0.0
 
    }()
    go func() {
        fmt.Println("TT", <-ch3)
        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)
}


В чем суть этой оптимизации?

1. Добавлен канал ch3. Этот канал блокирует вторую гоурутину, до окончания первой гоурутины.
2. Так как вторая гоурутина не читает из канала ch1, то он блокирует первую гоурутину при заполнении. Поэтому ch1 увеличен до необходимого 1,000,000

То есть, код больше не параллелен, работает последовательно, а канал используется как массив. И конечно этот код не в состоянии использовать второе ядро. В контексте этого кода нельзя говорить о “идеальном ускорении в N раз“.

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

Update 2: Тесты на Go 1.1.2



тест номер один с буфером 1 (channel01.go)

	ch1 := make(chan chan int, 1)


1 поток
go runchannel01.go
NumCPU 4
66.0038
66.0038
67.0038
66.0038
67.0038
66.0038
65.0037
67.0038
67.0039
76.0043
Total: 0 673.0385000000001


4 потока
go run channel01.go
NumCPU 4
116.0066
186.0106
112.0064
117.0067
175.01
115.0066
114.0065
148.0084
133.0076
153.0088
Total: 0 1369.0782

Вывод: значительно лучше. Зачем ставить буфер 1 трудно представить, но возможно есть применение у такого буфера.

тест номер один с буфером 100 (channel01.go)

	ch1 := make(chan chan int, 100)


1 поток
go run channel01.go
NumCPU 4
16.0009
17.001
16.0009
16.0009
16.0009
16.0009
17.001
16.0009
17.001
16.0009
Total: 0 163.00930000000002


4 потока
go runchannel01.go
NumCPU 4
66.0038
66.0038
67.0038
66.0038
67.0038
66.0038
65.0037
67.0038
67.0039
76.0043
Total: 0 673.0385000000001

Вывод: в два раза хуже, чем версия 1.0.2

тест номер два (channel03.go)

1 поток
go run channel03.go
NumCPU 4
1568.0897


4 потока
go run channel03.go
NumCPU 4
12119.6932


Примерно так же как версия 1.0.2, но чуть лучше. 1:8 против 1:10
@pyra
карма
22,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (56)

  • +4
    Глубоко
  • +16
    Разубедили, не буду пробовать Go.
    • +7
      Зря, хороший язык.
    • НЛО прилетело и опубликовало эту надпись здесь
  • +11
    Долго думал что же нам показывает данный тест.
    Но знаете, судя по всему, он нам показывает то, что если в обычный синхронный код еще дополнительно запустить ненужный поток, при этом начать дергать локи для синхронизации — то станет работать медленно, т.к. все это дело и так изначально не параллелилось, а тут мы еще дополнительную работу начали делать.

    а потом я понял в чем дело, и видимо на данные грабли уже наступало много людей, вот на этом видео vimeo.com/49718712 объясняют, что вы поняли неправильно, и как надо делать.

    и да, данную версию кода можно сделать эквивалентной с GOMAXPROCS > 1, надо не давать второй гороутине лезть в канал до выполнения первой

    вот код: gist.github.com/cybergrind/6734605
    вот результаты:
    1 процесс
    kpi➜~/tmp» time go run tt.go [23:18:16]

    Total: 0 48.514956
    go run tt.go 0.29s user 0.04s system 98% cpu 0.333 total
    6 процессов
    kpi➜~/tmp» time go run tt.go [23:18:24]
    NumCPU 6
    TT 1

    Total: 0 46.893547
    go run tt.go 0.29s user 0.04s system 98% cpu 0.327 total
    • +4
      И еще, в го, если правильно подойти к вопросу и подумать над тем что надо, почти всегда можно дойти до идеального ускорения в N раз (по-количеству процессов), если задача имеет такое решение.

      Более того я убежден, что отсутствие вытесняющей многозадачности — очень хороший способ размять мозги, писать более быстрый и более правильный код.
      • –9
        Ни когда еще вроде, ни кого не минусовал. Ведь заблуждаться нормально, а вот плюсовать ерунду! Вот бы кого я пометил специальным значком.
    • 0
      ch1 := make(chan int, 1000000)
      

      миллион не жирно будет? По вашей логике следует не останавливаться, заменить канал ch1 на масив размеренности миллион записать в него, а потом прочитать. В реальном сервере в канал ch1 скорей всего пишут постоянно и вторая гоурутина будет обрабатывать данные по поступлении не дожидаясь конца. Например, первая горутина пишет температуру и время в канал, а вторая высчитывает скользящие среднее температуры.
      • +4
        Суть в том, что тут код который не надо параллелить. Он не параллелится — тут не надо двух гороутин. если без буффера в миллион — заменяем спавн второй гороутины на вызов функции из первой (что и происходит в вашем коде при GOMAXPROCS=1), и не заморачиваемся, надо отдавать себе отчет в том, что мы делаем и почему.
        • –2
          Этот код вообще то тест каналов, который ни чего не считает, кроме времени работы каналов в определенном режиме. Если использовать каналы как вы предлагаете, то каналы и гоурутины ненужны вовсе. можно обойтись массивом. Но это будет уже совсем другая история.
          • +6
            Ну если мы все понимаем, что в этом коде не так, то может тогда не будет у людей создавать иллюзию, что go плохой язык, или GOMAXPROCS>1 плохо. Можно ведь честно сразу заявить, что данный код приведен не из реальных проблем го, а скорее про то, как в нем можно стрелять себе в ногу на ровном месте если не понимать как это работает, ведь вполне однозначно понятно, что синхронизация — штука не бесплатная, и если играть без нее — то будет быстрее (хотя разве тут это еще не всем очевидно?)

            p.s. я читал код =)
            p.p.s. стоит хоть немного нагрузить больше код обрабатывающий ch1, и запустить побольше гороутин — и чудесным образом все начнет ускорятся относительно одного потока.

            ну и еще обидно за такие наезды на go на ровном месте
            • 0
              Что заставило вас думать, что я считаю, что Go плохой язык. Go отличный язык. Я же в первом абзаце все написал. Статья про производительность каналов.

              Где так могут активно использоваться каналы? в онлайн играх, чатах, или живой новостной ленте как в вконтакте. Есть много приложений когда с разных источников идет постоянно информация.

              В чем суть статьи в том, что она позволяет сделает кое какую оценку того как быстро будет работать эта мясорубка. 3000 вам мало. ПХП с Мускулом дадут наверное 100 ну 300 будет по геройски.
              • +5
                Я наверное уже слишком много раз повторился на разный лад, но еще раз попробую высказать свою точку зрения (в последний раз в этом топике, а то мы рискуем скатиться в слишком эмоциональные обсуждения).

                Что показал топик — что каналы с синхронизацией, штука не бесплатная (при одном потоке синхронизация на не нужна).
                Что происходит при приведенном коде, запущенном в одном потоке?
                1 кладем ch1 < — i
                2 т.к. достигли максимального буффера, возвращаем управление в главный поток исполнения
                3 ищем гороутину которая может что-либо сделать
                4 (это будет вторая гороутина, main заблокирована на ch2) забираем сообщение из < — ch1
                5 т.к. вторая гороутина блокируется на чтении — возвращаем управление в главный поток исполнения
                ....repeat…

                т.е. фактически, мы передаем через ch1 переменную для вызова функции (далеко не самым оптимальным способом)
                Да, у нас есть ускорение за счет того, что мы избавляемся от вызова локов на операциях с каналами. Но если в однопоточной схеме пойти дальше, и выбросить ненужное перебрасывание сообщений через каналы (заменив на функции) — будет еще быстрее

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

                а) не использовать каналы если затраты на синхронизацию и возврат управления > накладных расходов по обработке того, что отправляем в канал
                б) если пункт а не прокатывает — пытаемся сделать так, что бы эти накладные расходы настолько превышали расходны на синхронизацию, что мы могли бы не заморачиваться на этот счет. тут есть тоже несколько стратегий.
                б1) шлем реально «большие» — в плане затрат собщения (например массив за какое-то время)
                б2) делаем буфер на основном передающем канале, но с помощью второго канала лочим гороутины относительно друг друга (когда кто-то выгребает очередь — ему никто не мешает). это уже поможет не настолько сильно, но хотя бы гороутины не начнут драться за доступ к каналам

                p.s. примерно в районе 2.5к на ядро пролегает очень много ограничений в асинхронном программировании (цифра может, ясное дело, меняться в зависимости от мощности компа, у меня 2.5к была производительности твистеда на helloworld аппликейшне, да и еще что-то такое мелькало). думаю тут дело в затратах на использование сисколов при наивном подходе к использованию асинхронных примитивов и надо смотреть в strace на что уходит время, и далее думать над тюнингом системы. Но тут я могу заблуждаться, глубоко не влазил
    • 0
      для тех кто не читал предложенный код.
      В моем тесте запущенно две гоурутины. Одна пишет в канал другая из него читает. Они это делают параллельно.
      В моем примере я передал 1,000,000 значений.
      Это заняло 240 мс и 5359мс. Миллион был выбран как раз, что бы долго не ждать и оценить производительность работы канала в однопоточном и многопоточном режиме.
      Оптимизатор кода советует отказаться от параллельной работы гоурутин, сначала записать все данные в канал, а потом прочитать: Тогда ведь теряется весь смысл теста и вообще каналы и гоурутины не нужны.
      • +4
        Вы показали явным образом, как приложение может замедлиться из-за накладных расходов, которые возникли из-за ненужного распараллеливания задачи. Ненужным оно является просто потому, что в коде вообще нет элемента полезной нагрузки. Т.е. это такой бенчмарк нынешней реализации каналов в Go и не больше, я правильно понимаю?
        • +2
          да. второй абзац.
          Но если указать Go использовать 2 и больше потока, то Go начинает использовать блокировки и производительность каналов может падать.

          потом пример когда использование каналов при однопоточном и многопоточном исполнении показывает разную скорость
        • 0
          в последнем абзаце перед Update, я упомянул как повело себя реальное приложение в продакшине. 10-20 кратного падения не было, только 10%. Что тоже было неожиданно.
  • 0
    Хотелось бы ещё посмотреть код на асинхронных каналах.
    • 0
      я не понимаю о чем вы. Go генерит асинхронную программу из кода, который выглядит синхронно. по гуглил про асинхронные каналы в Go ни чего не нашел.
      • 0
        • 0
          select позволяет писать читать сразу в несколько каналов. Заблокировать может так же само как и просто < — select/default — можно запистаь и без select
          if len(ch1) > 0 {
            m := <- ch1
          }else{
           // default case
          }
          

          В моих примерах не блокировать рутину нет смысла. Уникальность select как раз в том, что можно заблокировать и ожидать ответа из двух каналов. Это сделать без select ни как.
          • +2
            Ну, то есть вы блокируете рутину, но ругаетесь на межпроцессорные блокировки?
            • 0
              рутина блокируется, но поток нет. Рутина это просто сопрограмма, и стек. В Go есть свой щедулер. В этом основной преимущество Go. Гоутины очень дешевы. Шедулер может быть ефективен при 200.000 рутин
  • +1
    Берем erlang, haskell, ocaml + lwt и не паримся.
    • 0
      Мне было бы очень интересно увидеть аналогичные алгоритмы на erlang и rust. Думаю, что по размеру и читаемости кода новичком erlang проиграет разгромно. И Go однозначно лидер. Но вот производительность может быть выше. Например в rust нет обще памяти, у каждой таски (аналог гоурутины) свой сборщик мусора, которого вообще можно выключить!!! Если будет свободное время изучить rust то обязательно сравню.
      • +1
        rust вообще не стоит рассматривать до тех пор, пока он не стабилизируется. А то постоянно добавляют, меняют и все ломают, т.к. четкой спецификации у них нет.
        • 0
          rust в продакшине может и очень смело, сравнение rust против go будет очень интересно посмотреть на цифры
          • 0
            rust в продакшине

            Нестабильным языком (который могут полностью «сломать» одним коммитом) без четких целей, без четких определений что принимать, а что нет, в продакшене будет пользоваться только сумасшедший.
      • 0
        Про rust я не говорил. А взять тот же erlang, алгоритм чего вы хотите увидеть? Там и так все прекрасно с решением той задачи, которая описана в этом посте + равномерная загрузка всех ядер.
  • +2
    Знаете, есть задачи где НУЖНО использовать распараллеливание, а есть где НЕ НУЖНО. В ваших примерах, вы вызываете всего-лишь две простые функции, где накладные расходы выйдут больше, ежели выполнение самой программы. С тем-же POSIX threads накладные расходы будут еще больше.
    • –1
      Вы хотите сказать, что есть хорошие и плохие программисты и их программы соответствующие? — Да я с вами согласен.
      Влияет производительность каналов на производительность реальных приложений? — Да.
      На какой производительности появляется этот эффект? 3000 запросов в секунду.
      Какие части программы используют каналы? драйвер базы Redis реализует connection pool и харанит в канале соединения с базой, handler веб запросов может отсылать запросы в канал и ждать ответа потом возвращать результат. Каналы в Go основной способ общения между гоурутинами.
      • 0
        Я еще раз повторю про сложность задачи. У вас она элементарная и накладные расходы гораздо больше времени выполнения программы, соответственно производительность падает.
        • +1
          Вы знаете, лучший способ замереть скорость работы каналов?
          Часто вычислительная часть приложения очень проста.
          Например, веб приложение раздает промо коды первым N посетителям страницы, но только 1 промокод на человека.
          Глянуть в map и вернуть след промокод из массива это очень быстро. Но все это можно делать в одной гоурутине поэтому веб хендлеры должны использовать каналы каналов для общения с главной гоурутиной, что работает c map
  • +3
    Кстати, какую версию Go вы использовали? Советую обновиться до 1.1.2, у меня на ней результаты практически не различаются.
    • –1
      Вы молодец! First Myth Bursted!!!
      make(chan int, 1)
      

      тормазит только в два раза, а не в 10
      второй пример работает примерно так же медленнее на порядок.
      Но в первом примере на Go 1.1.2 при
      make(chan int, 100)
      

      4 раза тормазит многопоточный код. (в было в два раза, изменение не в лучшую сторону)
      make(chan int, 100) более типично чем make(chan int, 1)
      make(chan int, 1) — имеет смысл например в примере каналом каналов, но там соотношение осталось. 1:10
      Изменения в Go происходят.
      В продакшине используем Go 1.0.2 так как эта версия ставится в убунту 13.04 по умолчанию.
      Чуть поже наверное обновлю топик новыми результатами
      • 0
        Проверил на версии 1.1.1 — оба примера замедляются примерно в 2 раза при любом значении GOMAXPROCS > 1. Хочется отдельно отметить, что Go версии 1.0 даже не позиционировал себя, как супер-оптимизированный язык, и в Go 1.1 как раз было очень много изменений, улучшающих производительность.
        • 0
          Я обновил тесты для 1.1.2 они в последнем Update
          У вас такие же цифры?
          • 0
            Для второго теста (channel03.go) получается следующее (у меня 8 HT ядер, то есть 4 физических ядра. Результаты, видимо, зависят от того, на какие ядра попадает код):

            $ go run channel03.go
            619.561783

            $ GOMAXPROCS=8 go run channel03.go
            1595.614045
            $ GOMAXPROCS=8 go run channel03.go
            1588.348843
            $ GOMAXPROCS=8 go run channel03.go
            1330.332458
            $ GOMAXPROCS=8 go run channel03.go
            1145.460122
            $ GOMAXPROCS=8 go run channel03.go
            1146.408909
            $ GOMAXPROCS=8 go run channel03.go
            1129.536318
            $ GOMAXPROCS=8 go run channel03.go
            1146.533762

            P.S. Я немного изменил вашу программу, убрав оттуда лишние строки, поэтому использую просто переменные окружения :).
          • 0
            Ваш первый вариант, с небуферизированным каналом:

            $ go run channel01.go | tail -1
            Total: 131.99125800000002 0

            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 300.410302 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 309.75974299999996 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 302.951408 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 434.137033 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 530.844935 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 316.664217 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 499.2478709999999 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 445.700086 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 308.360513 0
            $ GOMAXPROCS=8 go run channel01.go | tail -1
            Total: 516.5550290000001 0
          • 0
            С буферизированным каналом (цифры при GOMAXPROCS=8 особо не скачут):

            $ go run channel01_chanbuf1.go | tail -1
            Total: 0 243.80800499999998

            $ GOMAXPROCS=8 go run channel01_chanbuf1.go | tail -1
            Total: 0 1084.379106
            $ GOMAXPROCS=8 go run channel01_chanbuf1.go | tail -1
            Total: 0 1015.814983
            $ GOMAXPROCS=8 go run channel01_chanbuf1.go | tail -1
            Total: 0 1062.683956
            • 0
              А почему вы не приводите?
              GOMAXPROCS=1 go run channel01_chanbuf1.go
              

              Я сравнивал два режима однопоточный и многопоточный. Когда используется два и более потока Go начинает использовать блокировки. О цене блокировок и была статья
              • 0
                В go 1.1 эта настройка до сих пор по умолчанию, поэтому первый результат без GOMAXPROCS — это и есть однопоточный вариант, к тому же выдающий всегда стабильное время.
                • 0
                  это вариант?
                  ch1 := make(chan chan int, 1)
                  

                  Я вчера добавил Update к статье для Go 1.1.2
                  Вы согласный с моими цифрами и выврдами?
                  • +1
                    Я привел результаты для всех 3 вариантов.

                    Первый вариант, оригинальный: время сильно прыгает, разница от 2,3 раз до 4 раз
                    Первый вариант, буферизированный канал: разница в 4,3 раза
                    Второй вариант (почему-то названный channel03.go): время сильно прыгает, разница от 1,8 раз до 2,6 раз (у вас — в 7,7 раз)

                    Цифры не сходятся, операционная система OS X 10.8.5, версия Go 1.1.1
                    • 0
                      Вчера второй тест 1.1.2 делал на Винде, а первый на маке. Сегодня до мака добрался. Действительно кардинальное изменение. У Мака цифры похожи на ваши.
                      Надо сделать еще один Update
                      Это какая фантастическая скорость.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      В моих приложениях большую часть времени занимает как раз передачи сообщений, а вычислений мало.
      Основные вычисления это добавить в map[string]Session новую сессию и обновить время посленего запроса (ping) в Session из map

      • НЛО прилетело и опубликовало эту надпись здесь
        • 0
          мютексы это не Go way. Они та есть, но с каналами код гораздо больше похож на то, что код делает.
          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              не только в Гоу, а вообще во многих языках общие объекты либо запрещены и нет мютексов или мувитоном являются.
              Каналы придуманы, что бы можно было не пользоваться мютексами
              golang.org/pkg/sync/
              • НЛО прилетело и опубликовало эту надпись здесь
                • +2
                  Вы правы, мьютексы существенно быстрее работают, чем каналы (в принципе, ожидаемо :)):

                  Исходный код теста
                  package main
                  
                  import (
                  	"fmt"
                  	"log"
                  	"sync"
                  	"time"
                  )
                  
                  const N = 10000000
                  
                  var (
                  	globalMutex sync.Mutex
                  	plusChan    = make(chan int)
                  	plusBufChan = make(chan int, 1000)
                  	mutableVar  = 0
                  )
                  
                  func emptyCycle() {
                  	for i := 0; i < N; i++ {
                  		// empty
                  	}
                  }
                  
                  func mutexCycle() {
                  	for i := 0; i < N/2; i++ {
                  		globalMutex.Lock()
                  		mutableVar++
                  		globalMutex.Unlock()
                  	}
                  }
                  
                  func chanCycle() {
                  	for i := 0; i < N; i++ {
                  		plusChan <- 1
                  	}
                  }
                  
                  func bufChanCycle() {
                  	for i := 0; i < N; i++ {
                  		plusBufChan <- 1
                  	}
                  }
                  
                  func main() {
                  	var start, emptyTime int64
                  
                  	start = time.Now().UnixNano()
                  	emptyCycle()
                  	emptyTime = time.Now().UnixNano() - start
                  	fmt.Printf("Empty:   %d ms\n", emptyTime/1e6)
                  
                  	start = time.Now().UnixNano()
                  
                  	mutableVar = 0
                  
                  	doneChan := make(chan int)
                  	for i := 0; i < 2; i++ {
                  		go func() {
                  			mutexCycle()
                  			doneChan <- 1
                  		}()
                  	}
                  
                  	for i := 0; i < 2; i++ {
                  		<-doneChan
                  	}
                  
                  	if mutableVar != N {
                  		log.Fatal("Inconsistent write for mutexCycle")
                  	}
                  
                  	fmt.Printf("Mutex:   %d ms\n", (time.Now().UnixNano()-start-emptyTime)/1e6)
                  
                  	incrChan := make(chan int)
                  
                  	go func() {
                  		for incr := range plusChan {
                  			mutableVar += incr
                  		}
                  
                  		incrChan <- 1
                  	}()
                  
                  	start = time.Now().UnixNano()
                  	mutableVar = 0
                  	chanCycle()
                  	close(plusChan)
                  	<-incrChan
                  
                  	if mutableVar != N {
                  		log.Fatal("Inconsistent write for chanCycle")
                  	}
                  
                  	fmt.Printf("Chan:    %d ms\n", (time.Now().UnixNano()-start-emptyTime)/1e6)
                  
                  	go func() {
                  		for incr := range plusBufChan {
                  			mutableVar += incr
                  		}
                  
                  		incrChan <- 1
                  	}()
                  
                  	start = time.Now().UnixNano()
                  	mutableVar = 0
                  	bufChanCycle()
                  	close(plusBufChan)
                  
                  	<-incrChan
                  	close(incrChan)
                  
                  	if mutableVar != N {
                  		log.Fatal("Inconsistent write for bufChanCycle")
                  	}
                  
                  	fmt.Printf("BufChan: %d ms\n", (time.Now().UnixNano()-start-emptyTime)/1e6)
                  }
                  

                  Результаты теста:

                  $ go version
                  go version go1.1.2 darwin/amd64
                  
                  $ time GOMAXPROCS=1 go run test.go
                  Empty:   3 ms
                  Mutex:   226 ms
                  Chan:    1625 ms
                  BufChan: 838 ms
                  
                  real	0m2.931s
                  user	0m2.872s
                  sys	0m0.048s
                  
                  $ time GOMAXPROCS=2 go run test.go
                  Empty:   3 ms
                  Mutex:   2395 ms
                  Chan:    6897 ms
                  BufChan: 1282 ms
                  
                  real	0m10.816s
                  user	0m14.480s
                  sys	0m2.751s
                  
                  • 0
                    Дороговато на первый взгляд. У вас однопоточная программа. Гоурутины это не потоки. Мютексы в однопоточной программе не делают вообще ни чего.
                    Вот я активировал 4 потока и добавил тест NChan в стиле cybergrind, в пустой цикл добавил nooptimizer++
                    Исходный код
                    package main
                    
                    import (
                        "fmt"
                        "log"
                        "sync"
                        "time"
                        "runtime"
                    )
                    
                    const N = 10000000
                    
                    var (
                        globalMutex sync.Mutex
                        plusChan    = make(chan int)
                        plusBufChan = make(chan int, 1000)
                        plusNChan = make(chan int, N+10)
                        mutableVar  = 0
                        nooptimizer = 0
                    )
                    
                    func emptyCycle() {
                        for i := 0; i < N; i++ {
                            // empty
                            nooptimizer++
                        }
                    }
                    
                    func mutexCycle() {
                        for i := 0; i < N/2; i++ {
                            globalMutex.Lock()
                            mutableVar++
                            globalMutex.Unlock()
                        }
                    }
                    
                    func mutexCycleM() {
                        for i := 0; i < N/2; i++ {
                            globalMutex.Lock()
                            mutableVar--
                            globalMutex.Unlock()
                        }
                    }
                    
                    func chanCycle() {
                        for i := 0; i < N; i++ {
                            plusChan <- 1
                        }
                    }
                    
                    func bufChanCycle() {
                        for i := 0; i < N; i++ {
                            plusBufChan <- 1
                        }
                    }
                    
                    func bufNCycle() {
                        for i := 0; i < N; i++ {
                            plusNChan <- 1
                        }
                    }
                    
                    func main() {
                    	
                        numcpu := runtime.NumCPU()
                        fmt.Println("NumCPU", numcpu)
                        runtime.GOMAXPROCS(numcpu)
                        //runtime.GOMAXPROCS(1)
                    
                        
                        var start, emptyTime int64
                    
                        start = time.Now().UnixNano()
                        emptyCycle()
                        emptyTime = time.Now().UnixNano() - start
                        fmt.Printf("Empty:   %d ms\n", emptyTime/1e6)
                    
                        start = time.Now().UnixNano()
                    
                        mutableVar = 0
                    
                        doneChan := make(chan int)
                        for i := 0; i < 2; i++ {
                            go func() {
                                mutexCycle()
                                doneChan <- 1
                            }()
                        }
                    
                        for i := 0; i < 2; i++ {
                            <-doneChan
                        }
                    
                        if mutableVar != N {
                            log.Fatal("Inconsistent write for mutexCycle")
                        }
                    
                        fmt.Printf("Mutex:   %d ms\n", (time.Now().UnixNano()-start-emptyTime)/1e6)
                    
                        incrChan := make(chan int)
                    
                        go func() {
                            for incr := range plusChan {
                                mutableVar += incr
                            }
                    
                            incrChan <- 1
                        }()
                    
                        start = time.Now().UnixNano()
                        mutableVar = 0
                        chanCycle()
                        close(plusChan)
                        <-incrChan
                    
                        if mutableVar != N {
                            log.Fatal("Inconsistent write for chanCycle")
                        }
                    
                        fmt.Printf("Chan:    %d ms\n", (time.Now().UnixNano()-start-emptyTime)/1e6)
                    
                        go func() {
                            for incr := range plusBufChan {
                                mutableVar += incr
                            }
                    
                            incrChan <- 1
                        }()
                    
                        start = time.Now().UnixNano()
                        mutableVar = 0
                        bufChanCycle()
                        close(plusBufChan)
                    
                        <-incrChan
                        if mutableVar != N {
                            log.Fatal("Inconsistent write for bufChanCycle")
                        }
                    
                        fmt.Printf("BufChan: %d ms\n", (time.Now().UnixNano()-start-emptyTime)/1e6)
                        
                        
                        
                        
                        go func() {
                    //        for incr := range plusNChan {
                    //            mutableVar += incr
                    //        }
                    
                            incrChan <- 1
                        }()
                    
                        start = time.Now().UnixNano()
                        mutableVar = 0
                        bufNCycle()
                        close(plusNChan)
                    
                        <-incrChan
                        close(incrChan)
                    
                        if mutableVar != N {
                     //       log.Fatal("Inconsistent write for bufChanCycle")
                        }
                    
                        fmt.Printf("NChan: %d ms\n", (time.Now().UnixNano()-start-emptyTime)/1e6)
                    
                        
                    }


                    go run test.go
                    NumCPU 4
                    Empty:   23 ms
                    Mutex:   1923 ms
                    Chan:    6770 ms
                    BufChan: 1300 ms
                    NChan: 377 ms
                    

                    Empty стал 8 раз медленней
                    BufChan победил Мютексы
                    NChan — просто блестел
                    Хотя даже если не комментить в NChan вторую гоурутину, то он все равно лидер с буфером в 10000000
                    NChan: 927 ms
                    • +1
                      Ну NChan блестел потому, что оттуда не читали, что, очевидно, является откровенным «читерством» :). Мне был прежде всего интересен однопоточный вариант, потому что Go заточен под один поток. По времени исполнения (и из банальной логики) можно понять, что Mutex.Lock() вполне себе делает работу и в однопоточном сценарии, но в моем бенчмарке горутины-таки исполняются при этом последовательно. Решить эту проблему в приведенном бенчмарке, чтобы честно проверить работу мьютекса во время конкуренции за локи довольно сложно, к сожалению, поскольку переключение между горутинами происходит как раз не на мьютексах, а на каналах. Вероятно, отсутствие переключения между горутинами туда-сюда и является настоящей причиной высокой производительности мьютексов в однопоточном варианте.

                      Ну и в многопоточном варианте мьютекс очень уверенно выигрывает у небуферизированного канала (толку от буферизированного немного: как можно видеть, мне пришлось поставить синхронизирующий канал, чтобы вообще дождаться исполнения суммирующей горутины в буферизированном канале, что не требуется для небуферизированного). То есть, полноценная замена мьютексу — это всё-таки небуферизированный канал, который суммирует сразу, а не ждет, поэтому правильно сравнивать именно с ним. Остальное я привел для полноты картины, в том числе вариант с количеством ядер > 1.
                      • 0
                        Ваши слова подтверждают мою статью! Я статью написал, что бы показать, что Go силен именно в однопоточном режиме и если есть 8 ядер то бывает имеет смысл запустить 8 однопоточных процессов, чем включать 8 потоков в Go. Меня версия 1.1.2 приятно удивила. И вообще приятно общаться с людьми, который пишут когда понимают. Спасибо

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.