четверг, 19 марта 2020 г.

Гонки данных в Golang простыми словами

Гонка данных (data race) происходит, когда две go-процедуры (goroutines) одновременно обращаются к одной и той же переменной, и, по крайней мере, одно из обращений является записью.

Гонки данных довольно распространены и могут быть очень сложными для отладки.

Эта функция имеет гонку данных, и ее поведение не определено. Она может, например, напечатать число 1 (хотя задумывалось 2).

func race() {
    wait := make(chan struct{})
    n := 0
    go func() {
        n++ // чтение, приращение, запись
        close(wait)
    }()
    n++ // конфликт доступа
    <-wait
    fmt.Println(n) // Вывод: неопределен
}

Две goroutines, g1 и g2, участвуют в гонке, и нет никакого способа узнать, в каком порядке будут проходить операции. Следующее является одним из многих возможных результатов.

g1 g2
Читает значение 0 из n.
Читает значение 0 из n.
Увеличивает значение с 0 до 1.
Записывает 1 в n.
Увеличивает значение с 0 до 1.
Записывает 1 в n.
Печатает n, которое сейчас равно 1.

Название "гонка данных" (data race) вводит в заблуждение. Не только порядок операций не определен - очень мало гарантий. И компиляторы, и аппаратные средства часто переворачивают код с ног на голову и выворачивают его наизнанку для достижения лучшей производительности. Если вы посмотрите на поток в середине действия, вы можете увидеть почти все что угодно.

Как избежать гонок данных

Единственный способ избежать гонки данных - это синхронизировать доступ ко всем изменяемым данным, которые совместно используются потоками. Есть несколько способов добиться этого. В Go вы обычно используете канал или блокировку (lock). (Низкоуровневые механизмы доступны в пакетах sync и sync/atomic.)

Предпочтительным способом обработки одновременного доступа к данным в Go является использование канала для передачи фактических данных из одной go-процедуры в другую. Девиз: "Не общайтесь, делясь памятью; делитесь памятью, общаясь".

func sharingIsCaring() {
    ch := make(chan int)
    go func() {
        n := 0 // Локальная переменная видна только одной 
               // goroutine.
        n++
        ch <- n // Данные покидают одну goroutine...
    }()
    n := <-ch // ...и благополучно прибывает в другую.
    n++
    fmt.Println(n) // Вывод: 2
}

В этом коде канал выполняет двойную функцию:

  • он передает данные из одной goroutine в другую,
  • и он действует как точка синхронизации.

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

Модель памяти Go - условия, при которых считывания переменной в одной goroutine могут гарантированно наблюдать значения, полученные в результате записи в одну и ту же переменную в другой goroutine, - довольно сложна, но при условии, что вы совместно используете все изменяемые данные между goroutines через каналы, вы защищены от гонок данных.


Читайте также:


Купить gopher

Комментариев нет:

Отправить комментарий