MST

星途 面试题库

面试题:Go的sync.Once在并发环境下如何保证原子性

详细描述sync.Once在高并发场景下,是如何通过内部机制确保初始化操作的原子性,避免重复初始化问题。请从其数据结构和关键方法的实现角度进行分析。
22.4万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

sync.Once的数据结构

sync.Once的数据结构在Go语言源码中定义如下:

type Once struct {
    done uint32
    m    Mutex
}
  1. done字段:是一个uint32类型,用于标记初始化是否完成。值为0表示未初始化,非0表示已初始化。在多线程环境下,它可以通过原子操作进行读写,以确保不同协程对其状态的准确感知。
  2. m字段:是一个互斥锁Mutex。用于在初始化操作时进行同步,保证同一时间只有一个协程能够执行初始化代码。

关键方法Do的实现

sync.Once的核心方法是Do,其定义如下:

func (o *Once) Do(f func()) {
    if atomic.LoadUint32(&o.done) == 0 {
        // 初次检查,快速路径,减少锁竞争
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}
  1. 快速检查:在Do方法中,首先通过atomic.LoadUint32(&o.done)原子地读取done字段的值。如果done为0,说明初始化尚未完成,进入慢速路径doSlow;如果done非0,说明已经初始化过,直接返回,避免再次进入慢速路径带来的锁开销。这种设计利用了原子操作的高效性,对于已经初始化完成的情况,大大减少了锁竞争。
  2. 慢速路径:在doSlow方法中,首先获取互斥锁o.m,确保同一时间只有一个协程能够执行后续代码。然后再次检查o.done是否为0,这是因为在获取锁之前,可能有其他协程已经完成了初始化。如果o.done仍然为0,则执行初始化函数f(),并在函数结束后,通过atomic.StoreUint32(&o.done, 1)原子地将done设置为1,表示初始化完成。这样,通过双重检查锁定机制和原子操作,确保了在高并发场景下初始化操作的原子性,避免了重复初始化问题。