MST

星途 面试题库

面试题:Go语言sync.Once底层实现细节及扩展应用

深入剖析Go语言sync.Once的底层实现机制,包括涉及到的原子操作、状态机等原理。并且基于此实现一个可以支持多次初始化但保证特定条件下初始化安全性的扩展版本,详细说明设计思路和实现代码。
19.9万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

sync.Once底层实现机制

  1. 原子操作

    • sync.Once 主要依赖于 atomic 包的原子操作来保证并发安全。它使用了 atomic.LoadUint32atomic.CompareAndSwapUint32 等操作。
    • sync.Once 结构体中,有一个 done 字段,类型为 uint32。当 done 为 0 时,表示尚未初始化;非 0 时,表示已经初始化。
    • atomic.LoadUint32 用于原子性地读取 done 的值,这样可以保证在并发环境下读取到的 done 值是准确的,不会出现脏读。
    • atomic.CompareAndSwapUint32 用于原子性地比较和交换 done 的值。只有当 done 的当前值等于预期值(通常为 0)时,才会将其设置为新值(通常为 1)。这个操作保证了在多个 goroutine 同时尝试初始化时,只有一个 goroutine 能够成功设置 done 为已初始化状态。
  2. 状态机

    • sync.Once 可以看作是一个简单的状态机。它有两个状态:未初始化(done == 0)和已初始化(done != 0)。
    • 初始状态为未初始化。当调用 Do 方法时,如果 done 为 0,即处于未初始化状态,就会尝试执行初始化函数。在执行初始化函数之前,会通过 atomic.CompareAndSwapUint32 尝试将 done 从 0 转换为 1(已初始化状态)。如果转换成功,则执行初始化函数;如果转换失败,说明其他 goroutine 已经完成了初始化,当前 goroutine 直接返回。

扩展版本设计思路

  1. 支持多次初始化

    • 为了支持多次初始化,我们不能仅仅依赖一个简单的 done 标志。可以引入一个计数器,记录初始化的次数。
    • 当计数器达到某个特定值时,才认为初始化完成,不再进行后续初始化。
  2. 保证特定条件下初始化安全性

    • 使用 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