## 9.4. 內存同步
你可能比較糾結爲什麽Balance方法需要用到互斥條件,無論是基於channel還是基於互斥量。畢竟和存款不一樣,它隻由一個簡單的操作組成,所以不會碰到其它goroutine在其執行"中"執行其它的邏輯的風險。這里使用mutex有兩方面考慮。第一Balance不會在其它操作比如Withdraw“中間”執行。第二(更重要)的是"同步"不僅僅是一堆goroutine執行順序的問題;同樣也會涉及到內存的問題。
在現代計算機中可能會有一堆處理器,每一個都會有其本地緩存(local cache)。爲了效率,對內存的寫入一般會在每一個處理器中緩衝,併在必要時一起flush到主存。這種情況下這些數據可能會以與當初goroutine寫入順序不同的順序被提交到主存。像channel通信或者互斥量操作這樣的原語會使處理器將其聚集的寫入flush併commit,這樣goroutine在某個時間點上的執行結果才能被其它處理器上運行的goroutine得到。
考慮一下下面代碼片段的可能輸出:
```go
var x, y int
go func() {
x = 1 // A1
fmt.Print("y:", y, " ") // A2
}()
go func() {
y = 1 // B1
fmt.Print("x:", x, " ") // B2
}()
```
因爲兩個goroutine是併發執行,併且訪問共享變量時也沒有互斥,會有數據競爭,所以程序的運行結果沒法預測的話也請不要驚訝。我們可能希望它能夠打印出下面這四種結果中的一種,相當於幾種不同的交錯執行時的情況:
```
y:0 x:1
x:0 y:1
x:1 y:1
y:1 x:1
```
第四行可以被解釋爲執行順序A1,B1,A2,B2或者B1,A1,A2,B2的執行結果。
然而實際的運行時還是有些情況讓我們有點驚訝:
```
x:0 y:0
y:0 x:0
```
但是根據所使用的編譯器,CPU,或者其它很多影響因子,這兩種情況也是有可能發生的。那麽這兩種情況要怎麽解釋呢?
在一個獨立的goroutine中,每一個語句的執行順序是可以被保證的;也就是説goroutine是順序連貫的。但是在不使用channel且不使用mutex這樣的顯式同步操作時,我們就沒法保證事件在不同的goroutine中看到的執行順序是一致的了。盡管goroutine A中一定需要觀察到x=1執行成功之後才會去讀取y,但它沒法確保自己觀察得到goroutine B中對y的寫入,所以A還可能會打印出y的一個舊版的值。
盡管去理解併發的一種嚐試是去將其運行理解爲不同goroutine語句的交錯執行,但看看上面的例子,這已經不是現代的編譯器和cpu的工作方式了。因爲賦值和打印指向不同的變量,編譯器可能會斷定兩條語句的順序不會影響執行結果,併且會交換兩個語句的執行順序。如果兩個goroutine在不同的CPU上執行,每一個核心有自己的緩存,這樣一個goroutine的寫入對於其它goroutine的Print,在主存同步之前就是不可見的了。
所有併發的問題都可以用一致的、簡單的旣定的模式來規避。所以可能的話,將變量限定在goroutine內部;如果是多個goroutine都需要訪問的變量,使用互斥條件來訪問。
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 査找重複的行
- GIF動畵
- 獲取URL
- 併發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 複數
- 布爾型
- 字符串
- 常量
- 複合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模闆
- 函數
- 函數聲明
- 遞歸
- 多返迴值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基於指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基於類型斷言識别錯誤類型
- 通過類型斷言査詢接口
- 類型分支
- 示例: 基於標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 併發的Clock服務
- 示例: 併發的Echo服務
- Channels
- 併發的循環
- 示例: 併發的Web爬蟲
- 基於select的多路複用
- 示例: 併發的字典遍歷
- 併發的退出
- 示例: 聊天服務
- 基於共享變量的併發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- sync.Once初始化
- 競爭條件檢測
- 示例: 併發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 爲何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value脩改值
- 示例: 解碼S表達式
- 獲取結構體字段標識
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附録