面试题答案
一键面试sync.Once底层实现机制
-
原子操作:
sync.Once
主要依赖于atomic
包的原子操作来保证并发安全。它使用了atomic.LoadUint32
和atomic.CompareAndSwapUint32
等操作。- 在
sync.Once
结构体中,有一个done
字段,类型为uint32
。当done
为 0 时,表示尚未初始化;非 0 时,表示已经初始化。 atomic.LoadUint32
用于原子性地读取done
的值,这样可以保证在并发环境下读取到的done
值是准确的,不会出现脏读。atomic.CompareAndSwapUint32
用于原子性地比较和交换done
的值。只有当done
的当前值等于预期值(通常为 0)时,才会将其设置为新值(通常为 1)。这个操作保证了在多个 goroutine 同时尝试初始化时,只有一个 goroutine 能够成功设置done
为已初始化状态。
-
状态机:
sync.Once
可以看作是一个简单的状态机。它有两个状态:未初始化(done == 0
)和已初始化(done != 0
)。- 初始状态为未初始化。当调用
Do
方法时,如果done
为 0,即处于未初始化状态,就会尝试执行初始化函数。在执行初始化函数之前,会通过atomic.CompareAndSwapUint32
尝试将done
从 0 转换为 1(已初始化状态)。如果转换成功,则执行初始化函数;如果转换失败,说明其他 goroutine 已经完成了初始化,当前 goroutine 直接返回。
扩展版本设计思路
-
支持多次初始化:
- 为了支持多次初始化,我们不能仅仅依赖一个简单的
done
标志。可以引入一个计数器,记录初始化的次数。 - 当计数器达到某个特定值时,才认为初始化完成,不再进行后续初始化。
- 为了支持多次初始化,我们不能仅仅依赖一个简单的
-
保证特定条件下初始化安全性:
- 使用
sync.Mutex
来保护对计数器和初始化状态的操作,确保在并发环境下的安全性。 - 同时,仍然需要原子操作来保证读取计数器和状态的一致性。
- 使用
实现代码
package main
import (
"fmt"
"sync"
"sync/atomic"
)
// MultiOnce 扩展版本的 sync.Once
type MultiOnce struct {
done uint32
count uint32
mutex sync.Mutex
limit uint32
fn func()
}
// NewMultiOnce 创建一个新的 MultiOnce 实例
func NewMultiOnce(limit uint32, fn func()) *MultiOnce {
return &MultiOnce{
done: 0,
count: 0,
limit: limit,
fn: fn,
}
}
// Do 执行初始化函数,支持多次初始化直到达到限制次数
func (m *MultiOnce) Do() {
if atomic.LoadUint32(&m.done) == 1 {
return
}
m.mutex.Lock()
defer m.mutex.Unlock()
if atomic.LoadUint32(&m.done) == 1 {
return
}
if atomic.LoadUint32(&m.count) < m.limit {
m.fn()
atomic.AddUint32(&m.count, 1)
if atomic.LoadUint32(&m.count) >= m.limit {
atomic.StoreUint32(&m.done, 1)
}
}
}
使用示例
func main() {
var mu sync.Mutex
var num int
initFn := func() {
mu.Lock()
num++
mu.Unlock()
fmt.Printf("Initializing, num = %d\n", num)
}
multiOnce := NewMultiOnce(3, initFn)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
multiOnce.Do()
}()
}
wg.Wait()
}
在上述代码中:
MultiOnce
结构体包含了done
标志、计数器count
、互斥锁mutex
、初始化次数限制limit
和初始化函数fn
。NewMultiOnce
函数用于创建MultiOnce
实例,传入初始化次数限制和初始化函数。Do
方法首先检查done
标志,如果已经完成初始化则直接返回。然后获取互斥锁,再次检查done
标志(防止在获取锁期间其他 goroutine 完成了初始化)。如果计数器小于限制次数,则执行初始化函数并增加计数器。当计数器达到限制次数时,设置done
标志。
在 main
函数中,通过创建多个 goroutine 并发调用 Do
方法来测试扩展版本的 MultiOnce
。