Гонка данных (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 через каналы, вы защищены от гонок данных.
Читайте также:
Комментариев нет:
Отправить комментарий