声明:本文是《Go并发编程实战》的样章,感谢图灵授权并发编程网站发布样章,禁止以任何形式转载此文。
现在,让我们再次聚焦到sync代码包。除了我们介绍过的互斥锁、读写锁和条件变量,该代码包还为我们提供了几个非常有用的API。其中一个比较有特色的就是结构体类型sync.Once和它的Do方法。
与代表锁的结构体类型sync.Mutex和sync.RWMutex一样,sync.Once也是开箱即用的。换句话说,我们仅需对它进行简单的声明即可使用,就像这样:
var once sync.Once
once.Do(func() { fmt.Println("Once!") })
如上所示,我们声明了一个名为once的sync.Once类型的变量之后,立刻就可以调用它的指针方法Do了。
该类型的方法Do可以接受一个无参数、无结果的函数值作为其参数。该方法一旦被调用,就会调用被作为参数传入的那个函数。从这一点看,该方法的功能实在是稀松平常。不过,重点并不在这里。
我们对一个sync.Once类型值的指针方法Do的有效调用次数永远会是1。也就是说,无论我们调用这个方法多少次,也无论我们在多次调用时传递给它的参数值是否相同,都仅有第一次调用是有效的。无论怎样,只有我们第一次调用该方法时传递给它的那个函数会被执行。请看下面的示例:
func onceDo() {
var num int
sign := make(chan bool)
var once sync.Once
f := func(ii int) func() {
return func() {
num = (num + ii*2)
sign <- true
}
}
for i := 0; i < 3; i++ {
fi := f(i + 1)
go once.Do(fi)
}
for j := 0; j < 3; j++ {
select {
case <-sign:
fmt.Println("Received a signal.")
case <-time.After(100 * time.Millisecond):
fmt.Println("Timeout!")
}
}
fmt.Printf("Num: %d.\n", num)
}
在onceDo函数中,我们利用for语句连续三次异步的调用once变量的Do方法。这三次调用传给Do方法的参数值都是相同的,都是变量fi所代表的匿名函数值。这个函数值的功能是先改变num变量的值再向非缓冲的sign通道发送一个true。变量num的值可以表示出once的Do方法被有效调用的次数,而通道sign则被用来传递代表了fi函数被执行完毕的信号。请注意,为了能够精确的表达出fi函数是在哪一次(或哪几次)调用once.Do方法的时候被执行的,我们在这里使用了闭包。在每次迭代之初,我们赋给fi变量的函数值都是对变量f所代表的函数值进行闭包的一个结果值。我们使用变量ii作为f函数中的自由变量,并在闭包的过程中把for代码块中的变量i的值加1后再与该自由变量绑定在一起。这样就生成了为当次迭代专门定制的函数fi。每次迭代中生成的fi函数在被执行的时候都会修改变量num的值。这些新的值不会出现重复,并且非常有助于我们倒推出所有的曾赋给自由变量的ii的值。这样,我们就可以知道哪个(或哪些)fi函数被真正的执行了。
函数onceDo中的第二条for语句的作用是等待之前的那三个异步调用的完成。读者可能已经发现,这两条for语句的预设迭代次数是一致的。在第二条for语句中,我们使用了select语句,并且为针对sign通道的接收操作设定了超时时间(100毫秒)。这是为了当永远无法从sign通道中接收元素值的时候不至于造成永久的阻塞。select语句中的每个case在被执行时都会打印出相应的内容。这有助于我们观察程序的实际运行情况。最后,我们还会打印出num变量的值。据此,我们可以判断在前面几次传递给Do方法的fi是否都被执行了。
在执行onceDo函数之后,我们会看到如下打印内容:
Received a signal.
Timeout!
Timeout!
Num: 2.
上面的打印内容表明,在成功从sign通道接收了一个元素值之后,出现了两次接收操作超时的情况。我们不用考虑在对sign通道的接收操作开始之时匿名函数fi还没有被执行完毕的情况。因为100毫秒的时间已经足够执行它很多很多次的了。因此,这两次接收操作超时应该是当时没有正在为此等待的对sign通道的发送操作导致的(注意,sign是一个非缓冲通道)。综上所述,我们可以初步判断,传递给once.Do方法的匿名函数fi只被执行了一次。并且,这仅有一次的执行的对象是在我们第一次调用该方法时传递给它的那个fi函数。
依据最后一行打印内容,我们可以证实上述判断。num变量的值为2意味着它只被修改了一次,并且是在自由变量ii为1的时候被修改的。这就可以证实,只有在for循环的第一次迭代时传递给once.Do方法的那个fi函数被执行了。这也符合sync.Once类型及其指针方法Do的语义。
请注意,这个仅被执行一次的限制只是针对单个sync.Once类型值来说的。换句话说,每个sync.Once类型值的指针方法Do都可以被有效的调用一次。
这个sync.Once类型的典型应用场景就是执行仅需执行一次的任务。例如,数据库连接池的初始化任务。又例如,一些心跳检测之类的实时监测任务。等等。
在一探sync.Once类型及其指针方法Do的内部实现之后,我们会有所发现:它们所提供的功能正是由前面讲到的互斥锁和原子操作来实现的。这个实现并不复杂。其使用的技巧包括卫述语句、双重检查锁定,以及对共享标记的原子读写操作。在熟知了本章讲述的这些同步工具之后,我们是否也能轻易设计出这样简单、有效的解决方案呢?
总之,sync.Once类型及其方法实现了“只会执行一次”的语义。我们在需要完成只需或只能执行一次的任务的时候应该首先想到它。