编程 Golang Sync.Once 使用与原理

2024-11-17 03:53:42 +0800 CST views 504

Golang Sync.Once 使用与原理,看这一篇足矣

1.1 简介

我们都知道 Go 中 init() 函数可以用于初始化,但是它会在包首次被加载时执行,如果未实际使用,不仅浪费内存,还会延缓程序启动时间。

sync.Once 可以在任何位置调用,它允许在实际依赖某个变量时再进行初始化,能实现延迟初始化,并且有效减少性能浪费。

更重要的是,sync.Once 是 Go 语言中的一个同步原语,并发安全。无论有多少个 goroutines 尝试同时执行某些代码,它都可以确保这些代码在并发环境下只执行一次。这在多线程环境中尤为重要,可防止初始化代码被执行多次。

适用场景:

  • 初始化全局资源
  • 数据库连接池的创建
  • 配置文件的加载
  • 等等

1.2 实战用法

1.2.1 单例模式

以下是使用 sync.Once 实现单例模式的示例:

package main

import (
	"fmt"
	"sync"
)

type Singleton struct{}

var singleton *Singleton
var once sync.Once

func GetSingletonObj() *Singleton {
	once.Do(func() {
		fmt.Println("Create Singleton")
		singleton = new(Singleton)
	})
	return singleton
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			obj := GetSingletonObj()
			fmt.Printf("%p\n", obj)
		}()
	}
	wg.Wait()
}

运行上述代码时,输出如下:

Create Singleton
0x574380
0x574380
0x574380
0x574380
0x574380

可以看到,无论有多少 goroutines,Create Singleton 只会执行一次。

1.2.2 关闭 channel

在 Go 语言中,尝试关闭已关闭的 channel 会导致 panic。为了确保 channel 只被关闭一次,可以使用 sync.Once

type SafeChannel struct {
	ch   chan int
	once sync.Once
}

// 安全关闭通道
func (sc *SafeChannel) Close() {
	sc.once.Do(func() {
		close(sc.ch)
	})
}

1.2.3 Std case

以下是 html.UnescapeString 函数的线程安全实现,它依赖 sync.Once 确保 entityentity2 字典只初始化一次:

func UnescapeString(s string) string {
	populateMapsOnce.Do(populateMaps)
	i := strings.IndexByte(s, '&')
	if i < 0 {
		return s
	}
	// 其他逻辑...
}

var populateMapsOnce sync.Once

func populateMaps() {
	entity = map[string]rune{
		"AElig;":  '\U000000C6',
		"AMP;":    '\U00000026',
		"Aacute;": '\U000000C1',
		// 其他实体定义...
	}
}

populateMapsOnce 确保字典只被初始化一次,从而避免在多线程情况下出现问题,并节省内存。

1.3 实现原理

1.3.1 内部结构

在 Go 源码中,sync.Once 的定义如下:

type Once struct {
	done atomic.Uint32
	m    sync.Mutex
}

1.3.2 基本实现

sync.Once 的核心方法是 Do,其实现如下:

func (o *Once) Do(f func()) {
	if o.done.Load() == 0 {
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done.Load() == 0 {
		defer o.done.Store(1)
		f()
	}
}
  • 快速路径:检查 done 的值是否为 1,如果是,说明已经执行过函数,直接返回。
  • 慢速路径:通过加锁确保只有一个 goroutine 执行 f(),并在执行后将 done 设置为 1。

1.3.3 实现特点

  • 原子操作atomic.Loadatomic.Store 用于检查和设置 done 标志,确保操作的原子性,避免竞态条件。
  • 双重检查:在获取锁之前先检查 done 值,避免不必要的锁竞争;获取锁后再检查,确保在并发环境下函数只执行一次。

1.4 自定义优化

可以自定义优化 sync.Once,例如:只有在没有错误的情况下才跳过函数执行。

package main

import (
	"errors"
	"fmt"
	"sync"
	"sync/atomic"
)

type Once struct {
	done atomic.Uint32
	m    sync.Mutex
}

func (o *Once) Do(f func() error) error {
	if o.done.Load() == 0 {
		return o.doSlow(f)
	}
	return nil
}

func (o *Once) doSlow(f func() error) error {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done.Load() == 0 {
		err := f()
		if err == nil {
			o.done.Store(1)
		}
		return err
	}
	return nil
}

func main() {
	once := Once{}
	var wg sync.WaitGroup
	onceFunc := func() error {
		fmt.Println("Initializing...")
		return errors.New("Error during initialization")
	}
	var globalErr error
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			if err := once.Do(onceFunc); err != nil {
				globalErr = err
			}
		}()
	}
	wg.Wait()
	fmt.Printf("Global error: %v", globalErr)
}

输出结果:

Initializing...
Initializing...
Initializing...
Initializing...
Initializing...
Global error: Error during initialization

1.5 注意事项

  • 合理评估需求,在非并发情况下使用 sync.Once 可能会浪费性能。
  • 不要在 Do 函数的 f 中嵌套调用 Do,这会导致死锁。
  • 避免拷贝 sync.Once,拷贝后可能导致多次执行函数。

1.6 总结

本文详细介绍了 Go 语言中的 sync.Once,包括其定义、使用场景、应用实例以及源码分析等。在实际开发中,sync.Once 常被用于单例模式和延迟初始化操作。本文还讲解了 sync.Once 源代码中的以下内容:

  • 原子操作的作用
  • 双重检查
  • 热路径(hot path)
  • CAS(Compare-And-Swap)
  • 不可拷贝性

我们还提供了一个简单优化版的 sync.Once,并给出了一些注意事项。

通过这些内容,你可以更好地理解和使用 sync.Once 来提高 Go 语言中的并发安全性和性能。

复制全文 生成海报 Go语言 并发编程 编程技巧

推荐文章

如何在 Vue 3 中使用 TypeScript?
2024-11-18 22:30:18 +0800 CST
php内置函数除法取整和取余数
2024-11-19 10:11:51 +0800 CST
任务管理工具的HTML
2025-01-20 22:36:11 +0800 CST
MySQL设置和开启慢查询
2024-11-19 03:09:43 +0800 CST
Go 中的单例模式
2024-11-17 21:23:29 +0800 CST
20个超实用的CSS动画库
2024-11-18 07:23:12 +0800 CST
php curl并发代码
2024-11-18 01:45:03 +0800 CST
Nginx 状态监控与日志分析
2024-11-19 09:36:18 +0800 CST
thinkphp swoole websocket 结合的demo
2024-11-18 10:18:17 +0800 CST
7种Go语言生成唯一ID的实用方法
2024-11-19 05:22:50 +0800 CST
Go 并发利器 WaitGroup
2024-11-19 02:51:18 +0800 CST
Golang 中你应该知道的 Range 知识
2024-11-19 04:01:21 +0800 CST
避免 Go 语言中的接口污染
2024-11-19 05:20:53 +0800 CST
html一个包含iPhoneX和MacBook模拟器
2024-11-19 08:03:47 +0800 CST
mysql int bigint 自增索引范围
2024-11-18 07:29:12 +0800 CST
html一份退出酒场的告知书
2024-11-18 18:14:45 +0800 CST
Vue3中如何处理状态管理?
2024-11-17 07:13:45 +0800 CST
Vue3中的事件处理方式有何变化?
2024-11-17 17:10:29 +0800 CST
如何实现生产环境代码加密
2024-11-18 14:19:35 +0800 CST
程序员茄子在线接单