Go в примерах: Атомарные счетчики (Atomic Counters)

Основным механизмом управления состоянием в Go является связь по каналам. Мы видели это, например, с пулами воркеров. Есть несколько других вариантов управления состоянием. Здесь мы рассмотрим использование пакета sync/atomic для атомарных счетчиков, к которым обращаются горутины.

package main
import (
    "fmt"
    "sync"
    "sync/atomic"
)
func main() {

Мы будем использовать целое число без знака для представления нашего (всегда положительного) счетчика.

    var ops uint64

WaitGroup поможет нам подождать, пока все горутины завершат свою работу.

    var wg sync.WaitGroup

Мы запустим 50 горутин, каждая из которых увеличивает счетчик ровно в 1000 раз.

    for i := 0; i < 50; i++ {
        wg.Add(1)

Для атомарного увеличения счетчика мы используем AddUint64, присваивая ему адрес памяти нашего счетчика ops с префиксом &.

        go func() {
            for c := 0; c < 1000; c++ {
                atomic.AddUint64(&ops, 1)
            }
            wg.Done()
        }()
    }

Ждем пока завершатся горутины.

    wg.Wait()

Теперь доступ к ops безопасен, потому что мы знаем, что никакие другие горутины не пишут в него. Безопасное чтение атомарного счетчика во время его обновления также возможно, используя функцию atomic.LoadUint64.

    fmt.Println("ops:", ops)
}

Мы ожидаем получить ровно 50 000 операций. Если бы мы использовали неатомарный ops++ для увеличения счетчика, мы бы, вероятно, получили другое число, изменяющееся между прогонами, потому что горутины мешали бы друг другу. Более того, мы получим сбои в гонке данных при работе с флагом -race.

$ go run atomic-counters.go
ops: 50000

Далее мы рассмотрим мьютексы, еще один способ управления состоянием.

Следующий пример: Мьютексы (Mutexes).