8.4 只会执行一次
现在,让我们再次聚焦到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
语句连续3次异步地调用once
变量的Do
方法。这3次调用传给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
语句的作用是等待之前的那3
个异步调用的完成。读者可能已经发现,这两条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
类型及其方法实现了“只会执行一次”的语义。我们在需要完成只需或只能执行一次的任务的时候应该首先想到它。