Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go sync.Once

深入理解go sync.Once的具体使用

作者:rubys007

在很多情况下,我们可能需要控制某一段代码只执行一次,go 为我们提供了 sync.Once 对象,它保证了某个动作只被执行一次,本文主要介绍了深入理解go sync.Once的具体使用,感兴趣的可以了解一下

在很多情况下,我们可能需要控制某一段代码只执行一次,比如做某些初始化操作,如初始化数据库连接等。

对于这种场景,go 为我们提供了 sync.Once 对象,它保证了某个动作只被执行一次。

当然我们也是可以自己通过 Mutex 实现 sync.Once 的功能,但是相比来说繁琐了那么一点,因为我们不仅要自己去控制锁,还要通过一个标识来标志是否已经执行过。

Once 的实现

Once 的实现非常简单,如下,就只有 20 来行代码,但里面包含了 go 并发、同步的一些常见处理方法。

package sync

import (
	"sync/atomic"
)

type Once struct {
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 {
		o.doSlow(f)
	}
}

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

简要说明:

使用示例

在下面的例子中,once.Do(test) 被执行了 3 次,但是最终 test 只被执行了一次。

package sync

import (
	"fmt"
	"sync"
	"testing"
)

var once sync.Once
var a = 0

func test() {
	a++
}

func TestOnce(t *testing.T) {
	var wg sync.WaitGroup
	wg.Add(3)

	for i := 0; i < 3; i++ {
		go func() {
			// once.Do 会调用 3 次,但最终只会执行一次
			once.Do(test)

			wg.Done()
		}()
	}

	wg.Wait()

	fmt.Println(a) // 1
}

Once 的一些工作机制

// 错误实现,这不能保证 f 只被执行一次
if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
    f()
}
config.once.Do(func() { config.init(filename) })

Once 详解

hotpath

这里说的 hotpath 指的是 Once 里的第一个字段 done

type Once struct {
	// hotpath
	done uint32
	m    Mutex
}

Once 结构体的第一个字段是 done,这是因为 done 的访问是远远大于 Once 中另外一个字段 m 的,
放在第一个字段中,编译器就可以做一些优化,因为结构体的地址其实就是结构体第一个字段的地址,
这样一来,在访问 done 字段的时候,就不需要通过结构体地址 + 偏移量的方式来访问,
这在一定程度上提高了性能。

结构体地址计算示例:

type person struct {
	name string
	age  int
}

func TestStruct(t *testing.T) {
	var p = person{
		name: "foo",
		age:  10,
	}
	// p 和 p.name 的地址相同
	// 0xc0000100a8, 0xc0000100a8
	fmt.Printf("%p, %p\n", &p, &p.name)

	// p.age 的地址
	// 0xc0000100b8
	fmt.Printf("%p\n", &p.age)
	// p.age 的地址也可以通过:结构体地址 + age 字段偏移量 计算得出。
	// 0xc0000100b8
	fmt.Println(unsafe.Add(unsafe.Pointer(&p), unsafe.Offsetof(p.age)))
}

atomic.LoadUint32

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 {
		o.doSlow(f)
	}
}

在 Do 方法中,是通过 atomic.LoadUint32 的方式来判断 done 是否等于 0 的,
这是因为,如果直接使用 done == 0 的方式的话,就有可能导致在 doSlow 里面对 done 设置为 1 之后,
在 Do 方法里面无法正常观测到。因此用了 atomic.LoadUint32

而在 doSlow 里面是可以通过 done == 0 来判断的,这是因为 doSlow 里面已经通过 Mutex 保护起来了。
唯一设置 done = 1 的地方就在临界区里面,所以 doSlow 里面通过 done == 0 来判断是完全没有问题的。

atomic.StoreUint32

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

在 doSlow 方法中,设置 done 为 1 也是通过 atomic.StoreUint32 来设置的。
这样就可以保证在设置了 done 为 1 之后,可以及时被其他 goroutine 看到。

Mutex

doSlow 的实现里面,最终还是要通过 Mutex 来保护临界区,
通过 Mutex 可以实现 f 只被执行一次,并且其他的 goroutine 都可以使用这一次 f 的执行结果。
因为其他 goroutine 在第一次 f 调用未返回之前,都阻塞在获取 Mutex 锁的地方,
当它们获取到 Mutex 锁的时候,得以继续往下执行,但这个时候 f 已经执行完毕了,
所以当它们获取到 Mutex 锁之后其实什么也没有干。

但是它们的阻塞状态被解除了,可以继续往下执行。

总结

到此这篇关于深入理解go sync.Once的具体使用的文章就介绍到这了,更多相关go sync.Once内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

您可能感兴趣的文章:
阅读全文