Даже со встроенной в Go поддержкой параллелизма ошибки все равно случаются. Легко случайно сослаться на переменную из двух разных горутин без получения блокировки. В компьютерных науках для этого используется термин гонка данных. Чтобы помочь найти такие ошибки, в Go есть проверка расы. Нет гарантии, что в вашем коде будет найдена каждая гонка данных, но если она найдется, вы должны установить соответствующие блокировки вокруг того, что она находит.
Давайте рассмотрим простой пример:
func getCounter() int { var counter int var wg sync.WaitGroup wg.Add(5) for i:=0;i<5;i++{ go func() { fori:=0;i<1000;i++{ counter++ } wg.Done() }() } wg.Wait() return counter }
Этот код запускает пять горутин, каждая из которых обновляет общую переменную счетчика 1000 раз, а затем возвращает результат. Вы ожидаете, что это будет 5000, поэтому давайте проверим это с помощью модульного теста:
func TestGetCounter(t *testing.T) { counter := getCounter() if counter != 5000 { t.Error("unexpected counter:", counter) } }
Если вы запустите go test несколько раз, вы увидите, что иногда он проходит успешно, но в большинстве случаев происходит сбой с сообщением об ошибке, например:
unexpected counter: 3673
Проблема в том, что в коде происходит гонка данных. В такой простой программе причина очевидна: несколько горутин пытаются обновить счетчик одновременно, и некоторые из их обновлений теряются. В более сложных программах такие гонки труднее увидеть. Давайте посмотрим, что делает средство проверки расы. Используйте флаг -race с go test, чтобы включить его:
$ go test -race ================== WARNING: DATA RACE Read at 0x00c000128070 by goroutine 10: test_examples/race.getCounter.func1() test_examples/race/race.go:12 +0x45 Previous write at 0x00c000128070 by goroutine 8: test_examples/race.getCounter.func1() test_examples/race/race.go:12 +0x5b
Следы дают понять, что строка counter++ является источником наших проблем.
Примечание:
Некоторые люди пытаются исправить условия гонки, вставляя в свой код «слипы», пытаясь разграничить доступ к переменной, к которой обращаются несколько горутин. Это плохая идея. Это может устранить проблему в некоторых случаях, но код по-прежнему неверен, и в некоторых ситуациях он не работает.
Подводя итог, вы также можете использовать флаг -race при сборке своих программ. Это создает двоичный файл, который включает средство проверки гонок и сообщает о любых гонках, которые он находит, на консоль. Это позволяет вам находить гонки данных в коде, в котором нет тестов.
Если средство проверки гонок так полезно, почему оно не включено постоянно для тестирования и производства? Двоичный файл с включенным параметром -race работает примерно в десять раз медленнее, чем обычный двоичный файл. Это не проблема для наборов тестов, выполнение которых занимает секунду, но для больших наборов тестов, выполнение которых занимает несколько минут, 10-кратное замедление снижает производительность.