介绍

sync.Once是一个简单而且强大的同步原语,使用它可以保证引用的函数只执行一次,经常在初始化配置时候用到该同步原语。

就它的用法看一个示例:

func main() {
 var once sync.Once
 for i := 0;i < 10;i++{
 go func() {
 once.Do(func() {
 fmt.Println("once内")
 })
 }()
 }
 time.Sleep(time.Second*5)
}
/*
输出结果:
once内
*/

可以看到,在并发情况下,该函数只执行了一次。当然,在非并发情况下同样保证只执行一次。


实现

接下来我们根据这个需求使用channel自己动手实现一个once。

首先定义结构体:

type OnceBak struct {
 c chan int _//通过接收的channel值来判断是否执行过方法_}

声明一个构造函数:

func NewOnce() *OnceBak {
 //构造一个有缓冲的通道,防止deadlock错误
 ch := make(chan int,1)
 //在channel发送一个数字1作为标志位
 ch <- 1
 return &OnceBak{
 c: ch,
 }
}

该结构体实现了一个Do方法,入参是一个函数:

func (o *OnceBak) Do(f func()) { 
 i := <- o.c 
 //判断接收到的是否为1,可以知道该方法是否执行过 
 if i == 1{ 
 //执行入参函数
 f() 
 //关闭channel的作用 
 // 1,防止channel读不到数据发生阻塞 
 //2,从关闭的管道里读到的int类型的默认值0 
 close(o.c) 
 }
}

来做一个小实验测试一下是否符合预期:

func main() {
 once := syn.NewOnce()
 for i := 0;i < 5;i++{
 go func() {
 once.Do(func() {
 fmt.Println("once中方法.....")
 })
 fmt.Println("once外方法-------")
 }()
 }
 time.Sleep(time.Second)
}
/*
测试结果:
once中方法.....
once外方法-------
once外方法-------
once外方法-------
once外方法-------
once外方法-------
*/

可以看到基本满足预期,Do方法里的函数只执行了一次,外面的方法依旧正常执行。


源码

源码中的sync.once也较为简单,贴出来阅读一下:

type Once struct { 
    done uint32 //标志位
    m Mutex //保证原子操作
}
/*
判断一下Once中的done标志位是不是默认值0
这里使用的atomic包保证了原子操作
*/
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()
 }
}

注意

注意,使用该原语要注意的是,该原语只保证sync.Once只计算Do被调用的次数,而不是调用传入Do的参数的次数,举个例子:

func main()  {
 var once sync.Once
 f1 := func() {fmt.Println("我是f1")}
 f2 := func() {fmt.Println("我是f2")}
 for i := 0;i < 10;i++{
 once.Do(f1)
 once.Do(f2)
 }
}
/*
该函数的输出结果是:

我是f1
*/

本次分享结束,继续享受剩余的假期。

image


郭朝
24 声望7 粉丝