Детектор гонок данных

Введение

Гонки данных являются одним из самых распространённых и трудных для отладки типов ошибок в параллельных системах. Гонка данных происходит, когда две горутины одновременно обращаются к одной и той же переменной, при этом хотя бы одно из обращений является записью. Подробнее смотрите в Модели памяти Go.

Вот пример гонки данных, которая может привести к сбоям и повреждению памяти:

func main() {
  c := make(chan bool)
  m := make(map[string]string)
  go func() {
    m["1"] = "a" // Первое конфликтующее обращение.
    c <- true
  }()
  m["2"] = "b" // Второе конфликтующее обращение.
  <-c
  for k, v := range m {
    fmt.Println(k, v)
  }
}

Использование

Для использования детектора гонок данных необходимо включить флаг -race при сборке программы:

go run -race main.go

Или при сборке:

go build -race main.go

Случайно разделяемая переменная

// ParallelWrite записывает данные в file1 и file2, возвращает ошибки.
func ParallelWrite(data []byte) chan error {
  res := make(chan error, 2)
  f1, err := os.Create("file1")
  if err != nil {
    res <- err
  } else {
    go func() {
      // Эта err разделяется с главной горутиной,
      // поэтому записьRace с записью ниже.
      _, err = f1.Write(data)
      res <- err
      f1.Close()
    }()
  }
  f2, err := os.Create("file2") // Вторая конфликтующая запись в err.
  if err != nil {
    res <- err
  } else {
    go func() {
      _, err = f2.Write(data)
      res <- err
      f2.Close()
    }()
  }
  return res
}

Исправление состоит в том, чтобы ввести новые переменные внутри горутин (обратите внимание на использование :=):

...
_, err := f1.Write(data)
...
_, err := f2.Write(data)
...

Незащищенная глобальная переменная

Если следующий код вызывается из нескольких горутин, то это приводит к гонкам на карте service. Одновременные чтения и записи одной и той же карты не являются безопасными:

var service map[string]net.Addr
func RegisterService(name string, addr net.Addr) {
  service[name] = addr
}
func LookupService(name string) net.Addr {
  return service[name]
}

Чтобы сделать код безопасным, защитите обращения с помощью мьютекса:

var (
  service   map[string]net.Addr
  serviceMu sync.Mutex
)
func RegisterService(name string, addr net.Addr) {
  serviceMu.Lock()
  defer serviceMu.Unlock()
  service[name] = addr
}
func LookupService(name string) net.Addr {
  serviceMu.Lock()
  defer serviceMu.Unlock()
  return service[name]
}

Незащищенная примитивная переменная

Гонки данных также могут происходить на переменных примитивных типов (bool, int, int64 и т.д.), как в этом примере:

type Watchdog struct{ last int64 }
func (w *Watchdog) KeepAlive() {
  w.last = time.Now().UnixNano() // Первое конфликтующее обращение.
}
func (w *Watchdog) Start() {
  go func() {
    for {
      time.Sleep(time.Second)
      // Второе конфликтующее обращение.
      if w.last < time.Now().Add(-10*time.Second).UnixNano() {
        fmt.Println("No keepalives for 10 seconds. Dying.")
        os.Exit(1)
      }
    }
  }()
}

Даже такие «безобидные» гонки могут привести к трудноотлаживаемым проблемам, вызванным небезопасностью операций с памятью, влиянием оптимизаций компилятора, или проблемами переупорядочивания обращений к памяти процессора.

Типичное исправление для этой гонки — использовать канал или мьютекс. Чтобы сохранить поведение без блокировок, можно также использовать пакет sync/atomic.

type Watchdog struct{ last int64 }
func (w *Watchdog) KeepAlive() {
  atomic.StoreInt64(&w.last, time.Now().UnixNano())
}
func (w *Watchdog) Start() {
  go func() {
    for {
      time.Sleep(time.Second)
      if atomic.LoadInt64(&w.last) < time.Now().Add(-10*time.Second).UnixNano() {
        fmt.Println("No keepalives for 10 seconds. Dying.")
        os.Exit(1)
      }
    }
  }()
}

Несинхронизированные операции отправки и закрытия

Как показывает этот пример, несинхронизированные операции отправки и закрытия на одном и том же канале также могут быть условием гонки:

c := make(chan struct{}) // или буферизованный канал
// Детектор гонок не может вывести отношение "происходит до"
// для следующих операций отправки и закрытия. Эти две операции
// не синхронизированы и происходят одновременно.
go func() { c <- struct{}{} }()
close(c)

Согласно модели памяти Go, отправка в канал происходит до соответствующего получения из этого канала завершено. Чтобы синхронизировать операции отправки и закрытия, используйте операцию получения, которая гарантирует, что отправка завершена до закрытия:

c := make(chan struct{}) // или буферизованный канал
go func() { c <- struct{}{} }()
<-c
close(c)

Требования

Детектор гонок требует включения cgo, а на системах, отличных от Darwin, требуется установленный компилятор C. Детектор гонок поддерживает linux/amd64, linux/ppc64le, linux/arm64, linux/s390x, linux/loong64, freebsd/amd64, netbsd/amd64, darwin/amd64, darwin/arm64, и windows/amd64.

В Windows, среда выполнения детектора гонок чувствительна к версии установленного компилятора C; начиная с Go 1.21, сборка программы с флагом -race требует компилятор C, включающий версию 8 или более позднюю библиотек mingw-w64. Вы можете проверить ваш компилятор C, вызвав его с аргументами --print-file-name libsynchronization.a. Более новая совместимая версия компилятора C напечатает полный путь к этой библиотеке, в то время как старые компиляторы C просто выведут аргумент.

Накладные расходы времени выполнения

Стоимость обнаружения гонок зависит от программы, но для типичной программы использование памяти может увеличиться в 5-10 раз, а время выполнения — в 2-20 раз.

Детектор гонок в настоящее время выделяет дополнительные 8 байт на каждую инструкцию defer и recover. Эти дополнительные выделения не освобождаются до завершения горутины. Это означает, что если у вас есть долгоживущая горутина, которая периодически выполняет вызовы defer и recover, использование памяти программы может расти без ограничений. Эти выделения памяти не будут отображаться в выводе runtime.ReadMemStats или runtime/pprof.

GoRu.dev Golang на русском

На сайте представлена адаптированная под русский язык документация языка программирования Golang