Goroutine变量赋值是否需同步?ErrGroup场景锁与内存可见性分析
你确实需要保留锁来保护这些赋值操作,核心原因不是互斥(毕竟两个子goroutine分别操作one和two,完全不会产生竞争),而是内存可见性——这也是你提到《Java Concurrency In Practice》里的核心点:
锁不仅用于互斥,还关乎内存可见性。为确保所有线程都能看到共享可变变量的最新值,读写线程必须在同一个锁上同步。
具体到你的Go代码场景:
这里存在三个goroutine:父goroutine和两个子goroutine。虽然errgroup.Wait()会阻塞父goroutine,直到两个子goroutine都执行完毕,但Go的内存模型不会自动保证子goroutine对共享变量的写入,在父goroutine中可见——除非通过同步原语(比如锁、channel操作)建立明确的happens-before关系。
你的代码里的锁完美解决了这个问题:
- 子goroutine在赋值
one/two后调用mu.Unlock() - 父goroutine在读取前调用
mu.Lock()
根据Go内存模型,Unlock操作happens before后续同一个锁的Lock操作,这就确保了父goroutine能看到子goroutine对one和two的最新赋值。
如果省略锁会发生什么?
如果去掉所有锁,即使g.Wait()等待子goroutine完成,父goroutine执行performAction(one, two)时,可能会读取到one或two的零值(nil)。这是因为CPU缓存的特性:子goroutine的赋值可能只存在于它所在CPU的本地缓存中,没有同步到主存;而父goroutine读取的是自己CPU缓存里的旧值,完全看不到子goroutine的更新。
总结
哪怕没有互斥竞争的需求,只要涉及到跨goroutine的共享变量读写,就需要用同步原语(比如这里的锁)来保证内存可见性,确保所有goroutine看到的变量值是一致的。
内容的提问来源于stack exchange,提问作者Sergii Getman

