用CAS操作实现Go标准库中的Once

Go标准库中提供了Sync.Once来实现“只执行一次”的功能。学习了一下源代码,里面用的是经典的双重检查的模式:

// Once is an object that will perform exactly one action.
type Once struct {
        m    Mutex
        done uint32
}

func (o *Once) Do(f func()) {
        if atomic.LoadUint32(&o.done) == 1 {
                return
        }
        // Slow-path.
        o.m.Lock()
        defer o.m.Unlock()
        if o.done == 0 {
                f()
                atomic.StoreUint32(&o.done, 1)
        }
}

我觉得这里也可以用Go的原子操作来实现“只执行一次”,代码更简单,而且比起用Mutex的开销要更小一些:

type Once struct {
        done int32
}

func (o *Once) Do(f func()) {
        if atomic.LoadInt32(&o.done) == 1 {
                return
        }
        // Slow-path.
        if atomic.CompareAndSwapInt32(&o.done, 0, 1) {
                f()
        }
}

熟悉Java内存模型的程序员可能会留意到上面的CAS操作中传递的是变量地址。如果在Java中,这样的变量是需要volatile来保证线程之间的可见性的,而Golang并没有volatile,Golang的内存模型的happen-after规则也没有提到atomic操作。但从Golang标准库的相关源码来看,Golang的atomic操作应该是可以满足可见性要求的。

从下面的测试上看,这样实现没有什么并发的bug。

     runtime.GOMAXPROCS(1000)
        n := 100000
        wg := new(sync.WaitGroup)
        wg.Add(n)
        
        a := int32(0)
        for i := 0; i < n; i++{
                go func(){
                        if atomic.CompareAndSwapInt32(&a, 0, 1) {
                                fmt.Println("Change a to 1")
                        }
                        wg.Done()
                }()
        }
        wg.Wait()

        fmt.Println("Test is done")

Go的内存模型文档中对atomic包并未提起,如果参考Java的来看