Go内存模型:如何正确同步地为字段赋值?
以下是我在Go中实现Promise的核心代码:
// A promise represents the future result of a call to a function. type promise struct { // state is the current state of this promise. state int32 // done is closed when execution completes to unblock concurrent waiters. done chan struct{} // the function that will be used to populate the outcome. function Function // outcome is set when execution completes. outcome Outcome } // get returns the value associated with a promise. // // All calls to promise.get on a given promise return the same result // but the function is called (to completion) at most once. // // - If the underlying function has not been invoked, it will be. // - If ctx is cancelled, get returns (nil, context.Canceled). func (p *promise) get(ctx context.Context) Outcome { if ctx.Err() != nil { return Outcome{ Value: nil, Err: ctx.Err(), } } if p.changeState(IsCreated, IsExecuted) { return p.run(ctx) } return p.wait(ctx) } // run starts p.function and returns the result. func (p *promise) run(ctx context.Context) Outcome { go func() { v, err := doExecute(ctx, p.function) p.outcome = Outcome{ Value: v, Err: err, } p.function = nil // aid GC close(p.done) }() return p.wait(ctx) } // wait waits for the value to be computed, or ctx to be cancelled. func (p *promise) wait(ctx context.Context) Outcome { select { case <-p.done: return p.outcome case <-ctx.Done(): return Outcome{ Value: nil, Err: ctx.Err(), } } } func (p *promise) changeState(from, to State) bool { return atomic.CompareAndSwapInt32(&p.state, int32(from), int32(to)) }
我看到Go内存模型中的一个示例,代码如下:
var a, b int func f() { a = 1 b = 2 } func g() { print(b) print(a) } func main() { go f() g() }
该示例说明g函数可能先打印2再打印0,文档中提到:
Programs with races are incorrect and can exhibit non-sequentially consistent executions. In particular, note that a read r may observe the value written by any write w that executes concurrently with r. Even if this occurs, it does not imply that reads happening after r will observe writes that happened before w.
我此前一直认为,在关闭done通道前设置变量,能保证其他协程看到该变量的最新值。但上述例子让我产生疑问:使用done通道是否真的有区别?其他协程会不会检测到done通道已关闭,却读取到尚未更新的字段值?
解答
你的认知是正确的,使用done通道的关闭操作与后续的接收操作,完全可以保证内存可见性。
根据Go内存模型的规则:
- 通道的关闭操作发生在因通道关闭而返回零值的接收操作完成之前。
- 在你的代码中,协程内先完成
p.outcome的赋值,再执行close(p.done),这两个操作在同一个协程内是顺序执行的,因此赋值操作发生在关闭操作之前。 - 当其他协程通过
<-p.done检测到通道关闭后,后续读取p.outcome的操作一定能看到之前赋值的最新值,不存在读取到旧值的情况。
而你提到的内存模型示例,问题在于代码中没有任何同步原语,两个协程的操作完全并发,没有明确的发生先后关系,因此才会出现非顺序一致的执行结果。这和你的Promise实现有本质区别——你的代码通过通道操作建立了明确的同步关系,不存在数据竞争的问题。
字段读写同步的正确方式
在Go中,保证并发场景下字段读写同步的常用方式包括:
- 通道操作:如你的实现中使用的关闭/接收通道,发送/接收通道都能建立同步关系,保证内存可见性。
- 原子操作:如你代码中使用的
atomic.CompareAndSwapInt32,适用于简单的数值类型读写同步。 - 互斥锁:使用
sync.Mutex或sync.RWMutex,通过加锁解锁操作建立同步,适合复杂的临界区场景。
内容的提问来源于stack exchange,提问作者Mr.J4mes

