一、场景

很多时候,我们希望加载配置时只加载一次,因为如连接数据库时较慢且不必加载多次;我们希望关闭通道时只关闭一次,因为关闭多次会报错;我们希望访问一个公共资源时,该资源是完整可靠的。

二、介绍

func (o *Once) Do(f func()) { }

三、问题的引出

下面模仿我们平常不正确的写法,现在有一个场景:有很多个协程要使用到配置资源,一般先判断里面是否为空,如果为空就要去加载配置资源,然后才能继续下面的任务,假设你是这样写的

package main

import (
	"fmt"
	"time"
)

var con map[string]string // 全局资源

func init_ziyuan() { // 加载配置资源
	con = map[string]string{"hello": "world"}  // 赋值
	time.Sleep(time.Second) // 模拟耗时操作
	con = map[string]string{"Areyou": "ok"}  // 赋值
}

func panduan(i int) { // 模拟多个协程访问公共资源
	if con == nil { // 如果配置资源没有加载就需要去加载
		init_ziyuan()
		fmt.Println(con)
	} else {
		fmt.Println(con, i) // 输出此时公共资源con的值
	}
}

func main() {
	for i := 0; i < 5; i++ { // 模拟多个协程同时访问公共资源
		go panduan(i)
	}
	time.Sleep(time.Second * 3) // 最笨地方法确保主函数不会提前退出
}
/* 结果
map[hello:world] 4
map[hello:world] 2
map[hello:world] 3
map[Areyou:ok]
map[Areyou:ok]
*/

由结果可知,0、1号协程执行的快,就去执行了加载资源配置,然后执行了配置资源中第一个赋值操作,2、3、4号协程执行的慢,此时0、1号协程已经完成赋值操作,因此con全局资源不为空,所以2、3、4号协程就执行了输出操作,而不执行加载资源配置。

这里就引发了问题:

  • 高并发时,多个协程访问的公共资源不一致

  • 加载配置资源的操作发生了两次甚至可能更多次

如果解决以上问题,我们可能想使用互斥锁对加载配置资源时的全局变量加锁,使得配置没有完成,其他协程就不能访问该资源。

但是,在加锁期间,其他协程由于不能访问公共资源而阻塞,这在高并发情况下是不好的,即影响性能,又由于阻塞而不能释放内存资源,导致内存瞬间爆满

四、sync.Once控制并发安全

在上面代码的基础上只增加了两行代码,如下。

package main

import (
	"fmt"
	"sync"
	"time"
)

var one sync.Once  // 定义全局结构体
var con map[string]string

func init_ziyuan() {
	con = map[string]string{"hello": "world"}
	time.Sleep(time.Second)
	con = map[string]string{"Areyou": "ok"}
}

func panduan(i int) {
	if con == nil {
		one.Do(init_ziyuan)  // 对资源配置函数调用
		fmt.Println(con)
	} else {
		fmt.Println(con, i)
	}
}

func main() {
	for i := 0; i < 5; i++ {
		go panduan(i)
	}
	time.Sleep(time.Second * 3)
}
/* 结果
map[hello:world] 2
map[hello:world] 4
map[hello:world] 3
map[hello:world] 0
map[Areyou:ok]
*/

注意:上面的代码在Windows中运行时,结果和错误的代码是一样的,我也不知道为什么。但在Linux环境下执行是对的。可在Golang官网在线执行https://golang.org/


原理(重点)

sync.Once的源码非常简短,也很简单,如下:

// once.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()
	}
}

done变量是一个没有符号的32位整形,m变量是一个互斥锁。调用Do( init_ziyuan )函数时,首先atomic.LoadUint32( )函数对done进行原子操作,相当于给done加了一个轻量级的锁,并之后返回done这个指针地址指向的值,由于没有初始化,所以它默认为零值,然后执行doSlow( )函数,在doSlow函数中加互斥锁,然后执行f( )函数,也就是我们传进来的init_ziyuan( )函数,执行完后对done进行修改值为1,并设置done的值为1,之后再解锁互斥锁

  • 可以发现done虽然不是一个布尔值,但由于原子操作需要用到int32类型,所以就当做布尔值使用

  • 可以发现互斥锁并不是对全局资源加锁,而是对函数进行加锁,使得函数在短时间内只执行一次

  • 可以发现done变量原子操作是用于保护互斥锁能正常加锁的