用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的来看

原文地址:https://www.cnblogs.com/liaofan/p/4010436.html