Is there a difference in Go between a counter using atomic operations and one using a mutex?

Viewed 10936

I have seen some discussion lately about whether there is a difference between a counter implemented using atomic increment/load, and one using a mutex to synchronise increment/load.

Are the following counter implementations functionally equivalent?

type Counter interface {
    Inc()
    Load() int64
}

// Atomic Implementation

type AtomicCounter struct {
    counter int64
}

func (c *AtomicCounter) Inc() {
    atomic.AddInt64(&c.counter, 1)
}

func (c *AtomicCounter) Load() int64 {
    return atomic.LoadInt64(&c.counter)
}

// Mutex Implementation

type MutexCounter struct {
    counter int64
    lock    sync.Mutex
}

func (c *MutexCounter) Inc() {
    c.lock.Lock()
    defer c.lock.Unlock()

    c.counter++
}

func (c *MutexCounter) Load() int64 {
    c.lock.Lock()
    defer c.lock.Unlock()

    return c.counter
}

I have run a bunch of test cases (Playground Link) and haven't been able to see any different behaviour. Running the tests on my machine the numbers get printed out of order for all the PrintAll test functions.

Can someone confirm whether they are equivalent or if there are any edge cases where these are different? Is there a preference to use one technique over the other? The atomic documentation does say it should only be used in special cases.

Update: The original question that caused me to ask this was this one, however it is now on hold, and i feel this aspect deserves its own discussion. In the answers it seemed that using a mutex would guarantee correct results, whereas atomics might not, specifically if the program is running in multiple threads. My questions are:

  • Is it correct that they can produce different results? (See update below. The answer is yes.).
  • What causes this behaviour?
  • What are the tradeoffs between the two approaches?

Another Update:

I've found some code where the two counters behave differently. When run on my machine this function will finish with MutexCounter, but not with AtomicCounter. Don't ask me why you would ever run this code:

func TestCounter(counter Counter) {
    end := make(chan interface{})

    for i := 0; i < 1000; i++ {
        go func() {
            r := rand.New(rand.NewSource(time.Now().UnixNano()))
            for j := 0; j < 10000; j++ {
                k := int64(r.Uint32())
                if k >= 0 {
                    counter.Inc()
                }
            }
        }()
    }

    go func() {
        prevValue := int64(0)
        for counter.Load() != 10000000 { // Sometimes this condition is never met with AtomicCounter.
            val := counter.Load()
            if val%1000000 == 0 && val != prevValue {
                prevValue = val
            }
        }

        end <- true

        fmt.Println("Count:", counter.Load())
    }()

    <-end
}
3 Answers

There is no difference in behavior. There is a difference in performance.

Mutexes are slow, due to the setup and teardown, and due to the fact that they block other goroutines for the duration of the lock.

Atomic operations are fast because they use an atomic CPU instruction (when possible), rather than relying on external locks to.

Therefore, whenever it is feasible, atomic operations should be preferred.

Atomics are faster in the common case: the compiler translates each call to a function from the sync/atomic package to a special set of machine instructions which basically operate on the CPU level — for instance, on x86 architectures, an atomic.AddInt64 would be translated to some plain ADD-class instruction prefixed with the LOCK instruction (see this for an example) — with the latter ensuring coherent view of the updated memory location across all the CPUs in the system.

A mutex is a much complicated thing as it, in the end, wraps some bit of the native OS-specific thread synchronization API (for instance, on Linux, that's futex).

On the other hand, the Go runtime is pretty much optimized when it comes to synchronization stuff (which is kinda expected — given one of the main selling points of Go), and the mutex implementation tries to avoid hitting the kernel to perform synchronization between goroutines, if possible, and carry it out completely in the Go runtime itself.

This might explain no noticeable difference in the timings in your benchmarks, provided the contention over the mutexes was reasonably low.


Still, I feel oblidged to note — just in case — that atomics and higher-level synchronization facilities are designed to solve different tasks. Say, you can't use atomics to protect some memory state during the execution of a whole function — and even a single statement, in the general case.

Related