Здесь у нас есть пример, предоставленный Go by Example
, чтобы объяснить атомный пакет.
https://gobyexample.com/atomic-counters
package main
import "fmt"
import "time"
import "sync/atomic"
func main() {
var ops uint64
for i := 0; i < 50; i++ {
go func() {
for {
atomic.AddUint64(&ops, 1)
time.Sleep(time.Millisecond)
}
}()
}
time.Sleep(time.Second)
opsFinal := atomic.LoadUint64(&ops) // Can I replace it?
fmt.Println("ops:", opsFinal)
}
Для atomic.AddUnit64
это просто понять.
Что касается операции read
, почему необходимо использовать atomic.LoadUnit
, а не читать этот счетчик напрямую?
Могу ли я заменить последние две строки следующими строками?
До
opsFinal := atomic.LoadUint64(&ops) // Can I replace it?
fmt.Println("ops:", opsFinal)
После
opsFinal := ops
fmt.Println("ops:", opsFinal)
Нас беспокоит этот сценарий?
Когда ЦП выполняет шаг 3, другая горутина может считать неполные и грязные данные из памяти. Так что использование atomic.LoadUint64
может избежать такой проблемы?
Являются ли операции чтения и записи для uint8 в golang атомарными?
Необходимо использовать atomic.LoadUint64
, потому что нет гарантии, что оператор :=
выполняет атомарное чтение.
В качестве примера рассмотрим теоретический случай, когда atomic.AddUint64
реализован следующим образом:
Если вы не используете atomic.LoadUint64
, возможно, вы читаете промежуточный результат между шагами 6 и 7.
На некоторых платформах (например, более старые процессоры ARM без встроенной поддержки 64-битных целочисленных операций) это вполне может быть реализовано описанным выше способом.
То же самое относится и к целым числам/указателям других размеров. Точное поведение будет зависеть от реализации пакета atomic
и архитектуры ЦП/памяти, на которой работает программа.