考虑下面这段代码会打印出什么:
```
var count int
increment := func() {
count++
}
var once sync.Once
var wg sync.WaitGroup
wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
defer wg.Done()
once.Do(increment)
}()
}
wg.Wait()
fmt.Printf("Count is %d\n", count)
```
你肯定已经注意到了sync.Once类型的变量,没错,这段代码将打印以下内容:
```
Count is 1
```
顾名思义,sync.Once确保了即使在不同的goroutine上,调用Do传入的函数只执行一次。
看起来将多次调用一个函数但执行一次的能力封装并放入标准库是一件奇怪的事情,但事实证明,对这种模式的需求相当频繁。为了好玩,让我们来检查Go的标准库,看看Go本身使用这个原语的频率。 这是一个将执行搜索的grep命令:
```
grep -ir sync.Once $(go env GOROOT)/src |wc -l
```
这会输出:
```
70
```
关于利用sync.Once有几点需要注意。我们来看看另一个例子。 你认为它会打印什么?
```
var count int
increment := func() { count++ }
decrement := func() { count-- }
var once sync.Once
once.Do(increment)
once.Do(decrement)
fmt.Printf("Count: %d\n", count)
```
这会输出:
```
Count: 1
```
输出显示1而不是0令人惊讶吗? 这是因为sync.Once只计算Do被调用的次数,而不是调用传入Do的唯一函数的次数。 通过这种方式,sync.Once的副本与被用于调用的函数紧密耦合;我们再次看到sync包内如何在一个紧密的范围内发挥最佳效果。 我建议你通过在一个小的词法块中包装sync.Once的来形式化这种耦合:一个小型函数,或者通过包装在一个类型中。 那么下面这个例子呢? 你认为会发生什么?
```
var onceA, onceB sync.Once
var initB func()
initA := func() { onceB.Do(initB) }
initB = func() { onceA.Do(initA) } // 1
onceA.Do(initA) // 2
```
1. 这里的调用无法执行,直到2被返回。
这段程序会发生死锁,因为在1处对Do的调用不会执行直到2执行完毕,而2处无法结束执行——这是一个标准的死锁示例。这可能有点反直觉,看起来好像我们正在使用sync.Once来防止多个初始化。有时候程序出现死锁正是由于逻辑中出现了循环引用。
* * * * *
学识浅薄,错误在所难免。我是长风,欢迎来Golang中国的群(211938256)就本书提出修改意见。
- 前序
- 谁适合读这本书
- 章节导读
- 在线资源
- 第一章 并发编程介绍
- 摩尔定律,可伸缩网络和我们所处的困境
- 为什么并发编程如此困难
- 数据竞争
- 原子性
- 内存访问同步
- 死锁,活锁和锁的饥饿问题
- 死锁
- 活锁
- 饥饿
- 并发安全性
- 优雅的面对复杂性
- 第二章 代码建模:序列化交互处理
- 并发与并行
- 什么是CSP
- CSP在Go中的衍生物
- Go的并发哲学
- 第三章 Go的并发构建模块
- Goroutines
- sync包
- WaitGroup
- Mutex和RWMutex
- Cond
- Once
- Pool
- Channels
- select语句
- GOMAXPROCS
- 结论
- 第四章 Go的并发编程范式
- 访问范围约束
- fo-select循环
- 防止Goroutine泄漏
- or-channel
- 错误处理
- 管道
- 构建管道的最佳实践
- 便利的生成器
- 扇入扇出
- or-done-channel
- tee-channel
- bridge-channel
- 队列
- context包
- 小结
- 第五章 可伸缩并发设计
- 错误传递
- 超时和取消
- 心跳
- 请求并发复制处理
- 速率限制
- Goroutines异常行为修复
- 本章小结
- 第六章 Goroutines和Go运行时
- 任务调度