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

Как мы знаем, Golang является мощным языком программирования со встроенной параллелизмом. Мы можем одновременно выполнять функцию с другими функциями, создавая goroutine с помощью ключевого слова go. Когда несколько горожан делятся данными или переменными, мы можем с трудом предсказать условия гонки.

В этом блоге, я расскажу следующие моменты:

Что такое состояние параллельной обработки и как такое может произойти?

Как мы можем определить условия параллельной обработки?

Типичные параллельные обработки данных и как мы можем решить расовые условия?

Каково состояние скачек данных?

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

package main
import (
    "fmt"
    "sync"
)
func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func() {
            fmt.Println(i)
            wg.Done()
        }()
    }
    wg.Wait()
 }
}
В приведенном выше примере вы должны заметить, что мы запустили 5 функций и переменную доступа i внутри goroutine, но здесь мы столкнулись с параллельным состоянием обработки данных, потому что все функции читают данные из переменной i одновременно и в то же время для цикла записывали новое значение для переменной.

ВЫВОД ПРОГРАММЫ:

5 5 5 5 5


Как мы можем определить условия гонки?

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

Для использования встроенного детектора состояний вам нужно просто добавить флаг -race в команду go run:
Давай, беги на главной гонке.

Эта команда находит условие гонки данных в программе, если оно есть, и печатает стек ошибок, где происходит условие гонки.

Вывод выборки

$ go run -race  race_loop_counter.go
==================
WARNING: DATA RACE
Read at 0x00c0000a8020 by goroutine 7:
  main.main.func1()
      /home/-/goworkspace/src/example/race_loop_counter.go:13 +0x3c
Previous write at 0x00c0000a8020 by main goroutine:
  main.main()
      /home/-/goworkspace/src/example/race_loop_counter.go:11 +0xfc
Goroutine 7 (running) created at:
  main.main()
      /home/-/goworkspace/src/example/race_loop_counter.go:12 +0xd8
==================
==================
WARNING: DATA RACE
Read at 0x00c0000a8020 by goroutine 6:
  main.main.func1()

Goroutine 6 (running) created at:
  main.main()
      /home/-/goworkspace/src/example/race_loop_counter.go:12 +0xd8
==================
2 2 4 5 5 Found 2 data race(s)
exit status 66

Как мы можем это решить?

Как только вы, наконец, найдете состояния гонки, вы будете рады узнать, что Gо предлагает несколько вариантов, чтобы исправить это.

Роб Пайк очень верно сформулировал следующую фразу. Решение нашей проблемы заключается в этом простом заявлении.

"Не общайтесь, обмениваясь памятью; вместо этого, разделяйте память, общаясь." - Роб Пайк

1. Использовать канал для обмена данными
Далее следует простая программа, где goroutine получает доступ к переменной, объявленной в функции main, увеличивает её, а затем закрывает канал ожидания.

Между тем, основной поток также пытается увеличить ту же переменную: ждет, пока канал закроется, и затем печатает значение переменной.

Однако здесь условие гонки сгенерированно между main и goroutine, так как они оба пытаются увеличить одну и ту же переменную.

Пример проблемы:
package main
import "fmt"

func main() {
    wait := make(chan int)
    n := 0
    go func() {
        n++
        close(wait)
    }()
    n++
    <-wait
    fmt.Println(n)
}

Для решения данной проблемы мы будем использовать каналы.

Решение:

package main
import "fmt"
func main() {
    ch := make(chan int)
    go func() {
        n := 0
        n++
        ch <- n
    }()
    n := <-ch
    n++
    fmt.Println(n)
}

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

2) Использовать sync.Mutex 

Ниже приведена программа для получения общего числа чётных и нечётных чисел из массива целых чисел, нумерации Collection и хранения в структуре.

Пример проблемы: 

package main
import (
    "fmt"
    "sync"
)

type Counter struct {
    EvenCount int
    OddCount  int
}

var c Counter
func main() {
    numberCollection := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
    fmt.Println("Start Goroutine")
    var wg sync.WaitGroup
    wg.Add(11)
    for _, number := range numberCollection {
        go setCounter(&wg, number)
    }
    wg.Wait()
    fmt.Printf("Total Event Number is %v and Odd Number is %v\n", c.EvenCount, c.OddCount)
}
func setCounter(wg *sync.WaitGroup, number int) {
    defer wg.Done()
    if number%2 == 0 {
        c.EvenCount++
        return 
    }
         c.OddCount++
    
}

Вывод:

 Total Event Number is 5 and Odd Number is 6

Если программа проверяется флагом детектора условия гонки, то мы замечаем строку с.EvenCount++ и строку 31 c.OddCount++, генерирующие условия гонки, потому что все функции goroutine одновременно записывают данные в структурированный объект.

Решение:

Чтобы решить эту проблему, мы можем использовать sync.Mutex для блокировки доступа к структурированному объекту, как в следующем примере:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    EvenCount int
    OddCount  int
    mux       sync.Mutex
}

var c Counter

func main() {
    numberCollection := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}
    fmt.Println("Start Goroutine")
    var wg sync.WaitGroup
    wg.Add(11)
    for _, number := range numberCollection {
        go setCounter(&wg, number)
    }
    wg.Wait()
    fmt.Printf("Total Event Number is %v and Odd Number is %v\n", c.EvenCount, c.OddCount)
}
func setCounter(wg *sync.WaitGroup, number int) {
    defer wg.Done()
    c.mux.Lock()
    defer c.mux.Unlock()
    if number%2 == 0 {
        c.EvenCount++
 return 
    } 
        c.OddCount++
   }

3. Копирование переменной (если это возможно)

Пример проблемы:
package main
import (
    "fmt"
    "sync"
)
func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func() {
            fmt.Printf("%v ", i)
            wg.Done()
        }()
    }
    wg.Wait()
}

Решение: 
package main
import (
    "fmt"
    "sync"
)
func main() {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < 5; i++ {
        go func(j int) {
            fmt.Printf("%v ", j)
            wg.Done()
        }(i)
    }
    wg.Wait()
}

Заключение:
Предпочтительный способ обработки параллельного доступа к данным в Golang заключается в использовании канала и использования флага -race для создания отчёта гонки данных, что помогает избежать условий гонки.

Ответить