一、场景
很多时候,我们希望加载配置时只加载一次,因为如连接数据库时较慢且不必加载多次;我们希望关闭通道时只关闭一次,因为关闭多次会报错;我们希望访问一个公共资源时,该资源是完整可靠的。
二、介绍
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变量原子操作是用于保护互斥锁能正常加锁的