考慮下面這段代碼會打印出什么:
```
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運行時
- 任務調度