面试题答案
一键面试sync.Once原理
- 数据结构:
sync.Once
内部包含一个done
标志位和一个m
互斥锁。其结构体定义类似如下:
type Once struct {
done uint32
m Mutex
}
- 初始化逻辑:
done
标志位初始值为0,代表尚未执行初始化操作。done
使用uint32
类型主要是为了可以使用原子操作。 - Do方法:
Once.Do(f)
方法用于执行初始化函数f
。在调用Do
方法时,首先会使用原子操作检查done
标志位。如果done
为0,说明初始化操作还未执行,此时会锁定互斥锁m
,再次检查done
(防止在获取锁之前其他协程已经完成初始化),若done
仍为0,则执行传入的函数f
,执行完毕后将done
置为1,最后解锁互斥锁。如果done
已经为1,说明初始化操作已经执行过,直接返回,不再执行f
。
正确使用方式
- 单例模式示例:
package main
import (
"fmt"
"sync"
)
type Singleton struct {
data string
}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{
data: "initial data",
}
})
return instance
}
在上述代码中,GetInstance
函数使用sync.Once
来确保instance
只被初始化一次。无论有多少个协程同时调用GetInstance
,instance
只会被创建一次。
并发场景下可能面临的问题及解决方法
- 问题:如果
Once.Do
方法中执行的初始化函数f
发生恐慌(panic),done
标志位仍会被设置为1,后续再次调用Do
方法时不会重新执行f
。这可能导致程序处于错误状态,因为初始化未正确完成。 - 解决方法:在初始化函数
f
内部进行适当的错误处理,避免恐慌。如果无法避免恐慌,可以通过封装Once.Do
调用,在外部捕获恐慌并进行相应处理。例如:
func SafeDo(once *sync.Once, f func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic in Once.Do: %v", r)
}
}()
once.Do(f)
return nil
}
然后在使用时:
func main() {
var once sync.Once
err := SafeDo(&once, func() {
// 可能发生恐慌的初始化操作
panic("simulated panic")
})
if err != nil {
fmt.Println("Error:", err)
}
}
这样可以在Once.Do
发生恐慌时,捕获并处理恐慌,避免done
标志位被错误设置导致后续问题。