首页
技术小册
AIGC
面试刷题
技术文章
MAGENTO
云计算
视频课程
源码下载
PDF书籍
「涨薪秘籍」
登录
注册
01 | Mutex:如何解决资源并发访问问题?
02 | Mutex:庖丁解牛看实现
03|Mutex:4种易错场景大盘点
04| Mutex:骇客编程,如何拓展额外功能?
05| RWMutex:读写锁的实现原理及避坑指南
06 | WaitGroup:协同等待,任务编排利器
07 | Cond:条件变量的实现机制及避坑指南
08 | Once:一个简约而不简单的并发原语
09 | map:如何实现线程安全的map类型?
10 | Pool:性能提升大杀器
11 | Context:信息穿透上下文
12 | atomic:要保证原子操作,一定要使用这几种方法
13 | Channel:另辟蹊径,解决并发问题
14 | Channel:透过代码看典型的应用模式
15 | 内存模型:Go如何保证并发读写的顺序?
16 | Semaphore:一篇文章搞懂信号量
17 | SingleFlight 和 CyclicBarrier:请求合并和循环栅栏该怎么用?
18 | 分组操作:处理一组子任务,该用什么并发原语?
19 | 在分布式环境中,Leader选举、互斥锁和读写锁该如何实现?
20 | 在分布式环境中,队列、栅栏和STM该如何实现?
当前位置:
首页>>
技术小册>>
Golang并发编程实战
小册名称:Golang并发编程实战
### 08 | Once:一个简约而不简单的并发原语 在Golang的并发编程世界中,`sync`包提供了多种同步机制,用于管理多个goroutine之间的协作与数据访问安全。其中,`sync.Once`是一个特别而强大的工具,它以极简的方式解决了程序中的一次性初始化问题,确保了某个操作(如初始化资源、配置环境等)在并发环境下只被执行一次。尽管其使用场景看似简单直接,但`sync.Once`背后的设计哲学和并发安全保证,使得它在复杂系统中扮演着不可或缺的角色。本章将深入探讨`sync.Once`的工作原理、使用场景、以及它如何以简约的方式解决复杂的并发问题。 #### 一、`sync.Once`的基本介绍 `sync.Once`是Golang标准库`sync`包中的一个结构体,它实现了`sync.Locker`接口(尽管其主要用途并非用于传统的锁机制),但最关键的是它提供了一个`Do(f func())`方法。`Do`方法接受一个无参数的函数`f`作为参数,并保证无论被调用多少次,`f`函数最多只会被执行一次。这一特性对于实现延迟初始化(Lazy Initialization)、单例模式等场景尤为有用。 ```go type Once struct { // 内部包含一个互斥锁和一个表示函数是否已执行的标志位 // 这里不直接展示其实现细节,因为它们是私有的 } func (o *Once) Do(f func()) { // 使用互斥锁和标志位确保f只执行一次 } ``` #### 二、`sync.Once`的工作原理 `sync.Once`之所以能保证函数只执行一次,主要依赖于其内部的互斥锁(通常是`sync.Mutex`或类似的同步机制)和一个标志位(通常是一个布尔值)。当`Do`方法首次被调用时,它会检查标志位以确定是否已执行过给定的函数。如果尚未执行,则锁定互斥锁,再次检查标志位(因为可能存在其他goroutine在第一次检查与锁定之间已经执行了函数的情况),如果仍未执行,则执行函数并设置标志位,最后解锁互斥锁。如果标志位已表明函数已执行,则直接返回,不执行任何操作。 这种双重检查锁定(Double-Checked Locking)模式有效减少了锁的使用,提高了性能,尤其是在高并发场景下。 #### 三、使用场景 `sync.Once`因其简洁性和高效性,在多种场景下得到了广泛应用: 1. **延迟初始化**:当某个资源或对象初始化代价较高,且不是所有情况下都需要立即使用时,可以使用`sync.Once`来确保该资源只被初始化一次,并在需要时提供访问。 2. **单例模式**:在并发环境下实现单例模式时,`sync.Once`提供了一种优雅且高效的解决方案,避免了复杂的锁管理和条件检查。 3. **日志系统初始化**:在大型应用中,日志系统通常在程序启动时初始化,但如果初始化依赖于某些外部条件(如配置文件加载完成),则可以使用`sync.Once`来延迟初始化,并确保即使在并发环境下也只初始化一次。 4. **配置加载**:类似地,应用配置也可能需要在程序的不同部分以并发方式访问,但配置本身只需要加载一次。`sync.Once`可以确保配置文件的读取和解析只发生一次。 5. **插件加载**:在插件化架构中,插件的加载和初始化可能需要跨多个goroutine同步进行,`sync.Once`可以用来确保每个插件只被加载和初始化一次。 #### 四、示例代码 以下是一个使用`sync.Once`实现单例模式的简单示例: ```go package main import ( "fmt" "sync" ) type Singleton struct{} var ( instance *Singleton once sync.Once ) func GetInstance() *Singleton { once.Do(func() { instance = &Singleton{} }) return instance } func main() { var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() fmt.Println(GetInstance()) }() } wg.Wait() } ``` 在这个例子中,无论我们启动多少个goroutine去调用`GetInstance()`,`Singleton`的实例`instance`都只会被创建一次。 #### 五、注意事项 尽管`sync.Once`非常强大且易于使用,但在使用时也需要注意以下几点: - **确保函数安全**:传递给`Do`方法的函数必须是并发安全的,因为它可能在多个goroutine中同时被调用(尽管实际上只执行一次)。然而,由于Go的内存模型,这种情况实际上不会发生,但函数内部的逻辑仍然需要保证在多线程环境下的正确性。 - **避免在函数中修改`sync.Once`的实例**:虽然这看起来有些多余,但强调一下,`sync.Once`的实例一旦创建,就不应该在其`Do`方法执行的函数中被修改或重新赋值,这可能导致未定义的行为。 - **考虑错误处理**:虽然`Do`方法本身不提供错误返回,但如果你传递给它的函数可能会失败(例如,由于资源获取失败),你需要在函数内部妥善处理这些错误,或者至少记录它们,以便后续分析。 #### 六、总结 `sync.Once`是Golang并发编程中一个看似简单却功能强大的工具。它以简约的方式解决了并发环境下的一次性初始化问题,为开发者提供了极大的便利。通过深入理解其工作原理和使用场景,我们可以更加灵活地运用这一并发原语,构建出更加健壮和高效的并发程序。无论是实现单例模式、延迟初始化,还是处理其他需要确保只执行一次的并发任务,`sync.Once`都是一个值得信赖的选择。
上一篇:
07 | Cond:条件变量的实现机制及避坑指南
下一篇:
09 | map:如何实现线程安全的map类型?
该分类下的相关小册推荐:
深入浅出Go语言核心编程(二)
Go语言从入门到实战
深入浅出Go语言核心编程(八)
Go Web编程(中)
从零写一个基于go语言的Web框架
Go 组件设计与实现
go编程权威指南(四)
深入浅出Go语言核心编程(三)
go编程权威指南(三)
深入浅出Go语言核心编程(六)
Go开发权威指南(上)
Go Web编程(下)