[go学习] go中的sync.Once实现

问题:为什么在实现的时候既用了lock又用了atomic?
先贴下源码

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sync

import (
	"sync/atomic"
)

// Once is an object that will perform exactly one action.
type Once struct {
     
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/x86),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32 // 标识是否执行完
	m    Mutex	// 信号量作为一个互斥锁
}

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// 	var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// 	config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) {
     
	// Note: Here is an incorrect implementation of Do:
	//
	//	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
     
	//		f()
	//	}
	//
	// Do guarantees that when it returns, f has finished.
	// This implementation would not implement that guarantee:
	// given two simultaneous calls, the winner of the cas would
	// call f, and the second would return immediately, without
	// waiting for the first's call to f to complete.
	// This is why the slow path falls back to a mutex, and why
	// the atomic.StoreUint32 must be delayed until after f returns.
	// 就是说注释的这种实现是有问题的,如果多个协程同时执行到这里,CAS是个乐观锁,当竞争胜利的人进入f的时候,f还未执行,
	// 然而竞争失败的协程就已经返回了,那么对失败的协程返回来说是错误的,因为此时f并没有被执行。
	// 因此要想办法把其他协程阻塞起来直到f执行完,这个时候就引入了done。
	// 但是问题来了,为什么不直接lock呢?
	if atomic.LoadUint32(&o.done) == 0 {
      //TODO-1
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
     
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
     //TODO-2
		defer atomic.StoreUint32(&o.done, 1) //TODO-3
		f()
	}
}

这里用了原子性和锁来实现的Once。
为什么不直接用锁来实现而多此一举加个原子性呢?像如下这样实现不行吗?

func (o *Once) Do(f func()){
     
	if o.done == 0{
     
		o.m.Lock()
		defer o.m.Unlock()
		if o.done == 0{
     
			f()
			o.done++
		}
	}
}

TODO-1

这里感觉是对的,但是官方给出了advice:https://golang.org/ref/mem.

Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.
To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.

意思就是说如果多协程同时修改数据,则多个协程之间必须实现序列化访问。要实现序列化,使用channel或者其他的同步操作例如sync和sync/atomic包实现。

因此,这里使用了atomic.LoadUint32来保证序列化的执行doSlow。

TODO-2

进入doSlow之后就用了锁解决冲突,然后这里边对o.done的读取是没有用atomic的,因为已经有锁保证了该冲突域只有一个协程执行,没必要再用atomic。

TODO-3

但是问题来了,为什么最后的写操作要用atomic呢?百思不得其姐。。。
这个我也查了些资料,比如:https://juejin.im/post/6844904018490163213
但是也没看懂也不知道正确性。希望有大佬来给出一个合理的解释。
至于我的想法,是这样的。就是说,首先它用atomic.LoadUint32来对多协程进行了序列化,因为要提供原子性,就必须使用atomic来对once进行读写操作,那么这里用Store就是为了保证原子性,让想读once的协程等待在那里,然后当我写完once以后,他们就可以直接获取到1然后返回。这样就那能避免了等待在原子读操作那里的协程进入doSlow占用了lock,增加了性能。

你可能感兴趣的:(go学习,go,golang,多线程,sync.Once)