Search code examples
gosynchronizationrace-conditiongoroutine

Is it safe to write to on-stack variables from different go routine blocking current one with WaitGroup?


There are various task executors, with different properties, and some of them only support non-blocking calls. So, I was thinking, whether there's a need to use mutex/channel to safely deliver task results to calling go-routine, or whether is it enough simple WaitGroup?

For sake of simplicity, and specificity of the question, an example using very naive task executor launching function directly as go routine:

func TestRace(t *testing.T) {
    var wg sync.WaitGroup

    a, b := 1, 2

    wg.Add(1)

    // this func would be passed to real executor
    go func() {
        a, b = a+1, b+1
        wg.Done()
    }()

    wg.Wait()

    assert.Equal(t, a, 2)
    assert.Equal(t, b, 3)
}

Execution of the test above with -race option didn't fail, on my machine. However, is that enough guarantee? What if go-routine is executed on different CPU core, or on CPU core block (AMD CCX), or on different CPU in multi-socket setups?

So, the question is, can I use WaitGroup to provide synchronization (block and return values) for non-blocking executors?


Solution

  • JimB should perhaps provide this as the answer, but I'll copy it from his comments, starting with this one:

    The WaitGroup here is to ensure that a, b = a+1, b+1 has executed, so there's no reason to assume it hasn't.

    [and]

    [T]he guarantees you have are laid out by the go memory model, which is well documented [here]. [Specifically, the combination of wg.Done() and wg.Wait() in the example suffices to guarantee non-racy access to the two variables a and b.]

    As long as this question exists, it's probably a good idea to copy Adrian's comment too:

    As @JimB noted, if a value is shared between goroutines, it cannot be stack-allocated, so the question is moot (see How are Go closures layed out in memory?). WaitGroup works correctly.

    The fact that closure variables are heap-allocated is an implementation detail: it might not be true in the future. But the sync.WaitGroup guarantee will still be true in the future, even if some clever future Go compiler is able to keep those variables on some stack.

    ("Which stack?" is another question entirely, but one for the hypothetical future clever Go compiler to answer. The WaitGroup and memory model provide the rules.)