有时,我们需要让某个函数只执行一次,那么如何来保证那?一般是我们自己写个原子变量来控制函数是否已经执行过,这样可行,但是需要我们自己实现这非业务逻辑的功能,其实go SDK中提供了sync.once()方法,帮我们封装了这层 *** 作。
二、自己实现函数只执行一次如下代码,我们在main方法内通过循环语句开启新goroutine异步调用sayHello()方法,运行后我们会看到输出100句 hello,world。
func sayHello() {
fmt.Println("hello,world")
}
func main() {
for i := 0; i < 100; i++ {
go sayHello()
}
// go中main goroutine退出,则整个进程就退出
//所以这里我们简单休眠,等待子goroutine执行完毕
time.Sleep(time.Minute)
}
如果我们想要实现虽然调用100次sayHello()但是只输出一条hello,world,一个可能的实现是这样的:
var flag int32
func sayHello() {
if atomic.CompareAndSwapInt32(&flag, 0, 1) {
fmt.Println("hello,world")
}
}
func main() {
for i := 0; i < 100; i++ {
go sayHello()
}
// go中main goroutine退出,则整个进程就退出
//所以这里我们简单休眠,等待子goroutine执行完毕
time.Sleep(time.Minute)
}
如上代码,我们创建了一个flag变量,然后再sayHello方法内使用CAS设置flag的值,由于flag的初始化值为0,所以当多个goroutine执行CAS时,只有一个goroutine可以把flag的值从0设置为1,其他的goroutine执行CAS会返回FALSE,这保证了只有一个goroutine可以打印hello,world.
上面的实现我们把CAS *** 作放到了sayHello方法内部了,假如其他函数也需要实现只执行一次我们还需要修改其函数内容,显然这不可取。所以我们打算抽取一个方法,让执行一次的逻辑独立出来,对所有函数都适用,一个可能的实现如下:
var flag int32
func onceCas(f func()) {
if atomic.CompareAndSwapInt32(&flag, 0, 1) {
f()
}
}
func main() {
for i := 0; i < 100; i++ {
go onceCas(sayHello)
}
}
如上代码,我们创建了onceCas(即基于CAS实现的once执行功能)函数,其入参为func(),内部则基于CAS保证函数f只执行一次。这看起来很好,确实可以解决上面的一类问题,但是下面让我们看一个例子,看看其存在的一些不足:
func onceCas(f func()) {
if atomic.CompareAndSwapInt32(&flag, 0, 1) {
f()
}
}
var u *user
func getInstance() {
u = &user{Name: "jiaduo"}
}
func main() {
//1.多goroutine并发调用获取实例
for i := 0; i < 100; i++ {
go onceCas(getInstance)
}
// 2如果flag为1则打印u实例
if flag == 1 {
fmt.Printf("user,%+v", u)
}
}
如上代码声明了一个全局的user变量u,getInstance函数用来创建一个实例,代码1则多goroutine并发调用onceCase函数,意图只调用getInstance一次,保证u的唯一性。代码2则判断如果flag值为1,则打印u的内容,那么问题是,如果flag确实为1,那么打印的u的内容一定是user,{Name:jiaduo}?
其实不然,因为如下onceCas函数的CAS *** 作只可以保证函数f只执行一次,但是不能保证CAS成功后也就是flag=1时,函数f已经被执行完毕了。
func onceCas(f func()) {
if atomic.CompareAndSwapInt32(&flag, 0, 1) {
f()
}
}
所以onceCas并不适合上面的场景。
那么假如我们不根据flag进行判断,而是根据u !=nil才打印那?
var u *user = nil
func getInstance() {
u = &user{Name: "jiaduo"}
}
func main() {
// 多goroutine并发调用获取实例
for i := 0; i < 100; i++ {
go onceCas(getInstance)
}
if u != nil {
fmt.Printf("user,%+v", u)
}
}
如上代码,我们可以保证 u != nil时,一定可以打印出user,{Name:jiaduo}?
答案其实也是否定的,这是因为函数getInstance中的
u = &user{Name: "jiaduo"}
*** 作不具有原子性,其执行可以分为三步:创建user对象分配内存,初始化user对象,然后把指针变量赋值为变量u。
由于指令重排序问题的存在,其执行顺序可能变成了:创建user对象分配内存,把指针变量赋值为变量u,初始化user对象。
所以即使我们判断 u!=nil但是打印出来的内容,可能还是u没初始化前的内容。
三、sync.once 3.1 sync.once 使用go sdk内提供了sync.once函数可以帮我们解上面的问题,其可以保证传入sync.once的函数只执行一次,并且可以保证传入的函数执行期间,其他调用该once函数的goroutine必须阻塞等到传入的函数执行完毕。
修改代码如下:
var once sync.Once
func main() {
// 多goroutine并发调用获取实例
for i := 0; i < 100; i++ {
go once.Do(getInstance)
}
if u != nil {
fmt.Printf("user,%+v", u)
}
}
如上我们通过调用once.Do方法保证getInstance方法只会被执行一次,并且在getInstance执行期间其他调用once.Do(getInstance)的goroutine会被阻塞,直到getInstance执行完毕。
3.2 sync.once 原理sync.once内部原理很简单,如下:
type Once struct {
//done变量用来标识函数是否执行完毕
done uint32
//m用来保证函数执行期间,其他goroutine阻塞
m Mutex
}
下面我们主看Do方法:
func (o *Once) Do(f func()) {
//1.判断函数是否执行过,done=0说明没执行过
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}
func (o *Once) doSlow(f func()) {
//2.加锁
o.m.Lock()
//6.函数返回后,释放锁
defer o.m.Unlock()
//3判断是否执行过
if o.done == 0 {
//5函数返回后,设置执行标识
defer atomic.StoreUint32(&o.done, 1)
//4具体执行函数
f()
}
}
如上代码1通过原子性方法LoadUint32判断标done是否为0,为0说明函数f还没被执行过,为1则说明f已经执行完毕。
如果done为0,则执行doSlow函数,代码2加锁保证多个goroutine执行时,只有一个goroutine可以执行f,其他goroutine要阻塞等待。
代码3双重检测判断函数f是否被执行过(因为阻塞的goroutine释放后也要执行该代码,所以需要判断),如果done=1说明f已经执行过,则返回,否则步骤4具体执行函数f。
在方法doSlow执行完毕后,会先执行步骤5调用原子性方法StoreUint32设置标done为1,标志函数f已经执行完毕。
然后执行步骤6释放锁,这时,其他阻塞的goroutine中会有一个goroutine获取到锁,然后执行步骤3,但是其发现done为1了,就直接返回了,然后会释放锁。这时,其他阻塞的goroutine中会有一个goroutine获取到锁,然后执行步骤3,但是其发现done为1了,就直接返回了,然后会释放锁...
一个问题,步骤5和步骤6是否可以互换顺序?答案是不可以。大家可以自行探究下,然后再评论区回复哦^^
四、总结当我们需要让某个函数只执行一次时,go sdk内置的sync.once()方法是不错的选择。其内部通过原子变量的load/store *** 作,以及锁保证传入sync.once的函数只执行一次,并且保证传入的函数执行期间,其他调用该once函数的goroutine必须阻塞等到传入的函数执行完毕
戳下面阅读
👇
我的第三本书 我的第二本书 我的第一本书
golang并发教程 ForkJoinPool K8s网络模型
我的视频号
点亮再看哦👇
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)