Bossy Lobster

A blog by Danny Hermes; musing on tech, mathematics, etc.

Edit on GitHub

Atomically Idempotent

Recently, I was analyzing some initialization code in Go with a teammate. The value being initialized was meant to be used in concurrent Go, so initialization had some requirement of atomicity. The code essentially boiled down to:

func (t *T) Start() {
    if atomic.LoadInt32(&t.State) == Started {
        return // Early Exit
    }

    atomic.StoreInt32(&t.State, Started)
    t.Starting <- Sentinel
}

However, we noted that this code is not truly atomic. The read and write of the State value are individually atomic, but they are not atomic together. One way to achieve atomicity here would be to just hold a mutex. However, our discussion led to another way to avoid the mutex and to continue to use the int32 state value.

Atomicity First

In order to ensure that our goroutine is the only one that can trigger the sentinel event to the Starting channel, we need an atomic compare-and-swap (CAS). This checks that our goroutine was the first to attempt to set the state to Started and only then send the sentinel value to the channel:

func (t *T) Start() {
    previous := atomic.LoadInt32(&t.State)
    if previous == Started {
        return
    }

    swapped := atomic.CompareAndSwapInt32(&t.State, previous, Started)
    if !swapped {
        return
    }
    t.Starting <- Sentinel
}

YMMV: Idempotent First

Depending on the design goals of the code, the idempotency may be a more crucial feature than the atomicity. For example, the code using a T can be constructed in a way that sending multiple sentinel values to the Starting channel is safely idempotent. In this case, a race between two goroutines with the first code would not be problematic because they could both send a sentinel.

Comments