Go Once 源码剖析

12-05 3,539 views

        很多时候程序执行时需要执行初始化操作,如果只需要执行一次即可。如果使用并发编程,则需要进行锁、逻辑判断等方式处理,处理起来比较麻烦。其实Go语言的sync标准库Once模块已经实现了该功能,今天就来说说Onde的具体实现。

        在源码剖析之前,咱们先通过一个实例,看看怎么使用。

package main

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

func do() {
    f := func() {
		fmt.Println(time.Now())
    }

    var once sync.Once

    for i := 0; i < 5; i++ {
		once.Do(f)
    }
}

func main() {
    do()
}

        do 函数仅被执行一次,这跟do函数没有任何关系。而是由Once.Do实现具体的控制,接下来看看Once.Do是如何实现的?

源码剖析

        Once的状态和f没啥关系,就算Do不同的f也没有用。因mutex的缘故,f不能调用同一once.Do,这会导致死锁。

// sync/once.go

type Once struct {
        m    Mutex		// 锁
        done uint32 	// 状态值
}

func (o *Once) Do(f func()) {
	// 判断o.done状态是否等于1
        if atomic.LoadUint32(&o.done) == 1 {
                return
        }

        // 加锁
        // 保证在该g执行期间,其它g不能执行
        o.m.Lock()
        defer o.m.Unlock()

        if o.done == 0 {
        	// 状态计数器改为1
                defer atomic.StoreUint32(&o.done, 1)
                
                // 执行函数
                f()			
        }
}

        Do函数进行了两次o.done状态的检查,为什么会进行两次检查哪?因为在o.m.Lock()之前无法保证同时只有一个g任务执行到这里,如果同时有两个g执行到o.m.Lock。此时只可能有一个g获取到锁,然后执行函数f,最后更改o.done状态为1。该g执行完成后,会unlock。此时另外一个g进行加锁,然后第二次执行f函数,这样是有问题的。所以在执行函数之前,需要对o.done的状态进行判断,这样就保证了f函数只会被执行一次。其实平时在应用开发中使用并发编程,二次检查是很有必要的。

        Once源码其实就这么简单,只有一个Do函数。

(完)