MST
星途 面试题库

面试题:Go语言指针操作与并发安全

在并发编程场景下,使用Go语言的指针操作(涉及unsafe包)实现一个线程安全的单例模式。要求考虑到Go语言的并发特性,避免竞态条件,同时要确保单例对象在第一次使用时才初始化,并且在多次初始化尝试中只有一次成功。请详细描述设计思路并完整实现代码。
39.0万 热度难度
编程语言Go

知识考点

AI 面试

面试题答案

一键面试

设计思路

  1. 使用unsafeunsafe包提供了对Go语言内存的底层操作,允许我们进行指针操作,这在实现单例模式时用于直接操作内存地址,以达到控制对象初始化的目的。
  2. 原子操作:为了避免竞态条件,使用atomic包中的原子操作函数。原子操作可以保证在多线程环境下对共享资源的操作是原子性的,即不会被其他线程打断。
  3. 延迟初始化:通过一个标志位来判断单例对象是否已经初始化,只有在未初始化时才进行初始化操作,确保单例对象在第一次使用时才初始化。

代码实现

package main

import (
	"atomic"
	"fmt"
	"unsafe"
)

// Singleton结构体定义单例对象
type Singleton struct {
	// 这里可以添加单例对象需要的属性
}

var (
	instance unsafe.Pointer
	initialized uint32
)

// GetInstance获取单例实例
func GetInstance() *Singleton {
	if atomic.LoadUint32(&initialized) == 1 {
		return (*Singleton)(instance)
	}

	// 这里使用atomic.CompareAndSwapUint32来确保只有一次初始化成功
	if atomic.CompareAndSwapUint32(&initialized, 0, 1) {
		// 使用unsafe包进行内存分配和初始化
		alloc := unsafe_Alloc(unsafe.Sizeof(Singleton{}))
		instance = alloc
		(*Singleton)(instance) = Singleton{}
		return (*Singleton)(instance)
	}

	// 如果初始化已经完成,再次等待并返回
	for atomic.LoadUint32(&initialized) != 1 {
		// 可以在这里使用runtime.Gosched()来让出CPU,避免空转
	}
	return (*Singleton)(instance)
}

测试代码

func main() {
	// 创建多个goroutine并发获取单例实例
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			s1 := GetInstance()
			s2 := GetInstance()
			fmt.Println(s1 == s2)
		}()
	}
	wg.Wait()
}

在上述代码中:

  1. instance是一个unsafe.Pointer类型的变量,用于存储单例对象的地址。
  2. initialized是一个uint32类型的标志位,用于标记单例对象是否已经初始化。
  3. GetInstance函数首先检查initialized标志位,如果已经初始化则直接返回单例对象。如果未初始化,则使用atomic.CompareAndSwapUint32尝试将initialized从0设置为1,如果设置成功则进行单例对象的初始化。如果设置失败则等待直到初始化完成。
  4. main函数中,通过多个goroutine并发调用GetInstance函数来测试单例模式的线程安全性。