Go 中一個非典型不加鎖讀寫變數案例分析
Originally published in liudanking.com
前段時間在 v2 看到一個關於併發讀寫變數的問題:go 一個執行緒寫, 另外一個執行緒讀, 為什麼不能保證最終一致性。帖子中給出的例子非常簡單(稍作修改)main.go
:
package main
import (
"fmt"
"runtime"
"time"
)
var i = 0
func main() {
runtime.GOMAXPROCS(2)
go func() {
for {
fmt.Println("i am here", i)
time.Sleep(time.Second)
}
}()
for {
i += 1
}
}
既然是問題貼,直接執行的結果應該是出乎大多數人預料的:
╰─➤ go run main.go 1 ↵
i am here 0
i am here 0
i am here 0
i am here 0
i am here 0
i am here 0
...
帖子的回覆比較多,涉及的資訊量相對雜亂,爬完樓反而感覺沒有看懂。這裡就不賣關子,直接給出脫水後的結論:出現上面結果的原因是 go 的編譯器把程式碼 i 自加 1 的 for 迴圈優化掉了。要驗證這一點也很簡單,我們使用 go tool objdump -s 'main\.main' main
檢視編譯出的二進位制可執行檔案的彙編程式碼:
╰─➤ go tool objdump -s 'main\.main' main
TEXT main.main(SB) /Users/liudanking/code/golang/gopath/src/test/main.go
main.go:11 0x108de60 65488b0c25a0080000 MOVQ GS:0x8a0, CX
main.go:11 0x108de69 483b6110 CMPQ 0x10(CX), SP
main.go:11 0x108de6d 7635 JBE 0x108dea4
main.go:11 0x108de6f 4883ec18 SUBQ $0x18, SP
main.go:11 0x108de73 48896c2410 MOVQ BP, 0x10(SP)
main.go:11 0x108de78 488d6c2410 LEAQ 0x10(SP), BP
main.go:12 0x108de7d 48c7042402000000 MOVQ $0x2, 0(SP)
main.go:12 0x108de85 e8366bf7ff CALL runtime.GOMAXPROCS(SB)
main.go:13 0x108de8a c7042400000000 MOVL $0x0, 0(SP)
main.go:13 0x108de91 488d05187f0300 LEAQ go.func.*+115(SB), AX
main.go:13 0x108de98 4889442408 MOVQ AX, 0x8(SP)
main.go:13 0x108de9d e8fe13faff CALL runtime.newproc(SB)
main.go:20 0x108dea2 ebfe JMP 0x108dea2
main.go:11 0x108dea4 e8c7dffbff CALL runtime.morestack_noctxt(SB)
main.go:11 0x108dea9 ebb5 JMP main.main(SB)
:-1 0x108deab cc INT $0x3
:-1 0x108deac cc INT $0x3
:-1 0x108dead cc INT $0x3
:-1 0x108deae cc INT $0x3
:-1 0x108deaf cc INT $0x3
TEXT main.main.func1(SB) /Users/liudanking/code/golang/gopath/src/test/main.go
main.go:13 0x108deb0 65488b0c25a0080000 MOVQ GS:0x8a0, CX
main.go:13 0x108deb9 483b6110 CMPQ 0x10(CX), SP
main.go:13 0x108debd 0f8695000000 JBE 0x108df58
main.go:13 0x108dec3 4883ec58 SUBQ $0x58, SP
main.go:13 0x108dec7 48896c2450 MOVQ BP, 0x50(SP)
main.go:13 0x108decc 488d6c2450 LEAQ 0x50(SP), BP
main.go:15 0x108ded1 0f57c0 XORPS X0, X0
main.go:15 0x108ded4 0f11442430 MOVUPS X0, 0x30(SP)
main.go:15 0x108ded9 0f11442440 MOVUPS X0, 0x40(SP)
main.go:15 0x108dede 488d059b020100 LEAQ runtime.types+65664(SB), AX
main.go:15 0x108dee5 4889442430 MOVQ AX, 0x30(SP)
main.go:15 0x108deea 488d0d0f2d0400 LEAQ main.statictmp_0(SB), CX
main.go:15 0x108def1 48894c2438 MOVQ CX, 0x38(SP)
main.go:15 0x108def6 488d1583fb0000 LEAQ runtime.types+63872(SB), DX
main.go:15 0x108defd 48891424 MOVQ DX, 0(SP)
main.go:15 0x108df01 488d1d107c0c00 LEAQ main.i(SB), BX
main.go:15 0x108df08 48895c2408 MOVQ BX, 0x8(SP)
main.go:15 0x108df0d e84eddf7ff CALL runtime.convT2E64(SB)
main.go:15 0x108df12 488b442410 MOVQ 0x10(SP), AX
main.go:15 0x108df17 488b4c2418 MOVQ 0x18(SP), CX
main.go:15 0x108df1c 4889442440 MOVQ AX, 0x40(SP)
main.go:15 0x108df21 48894c2448 MOVQ CX, 0x48(SP)
main.go:15 0x108df26 488d442430 LEAQ 0x30(SP), AX
main.go:15 0x108df2b 48890424 MOVQ AX, 0(SP)
main.go:15 0x108df2f 48c744240802000000 MOVQ $0x2, 0x8(SP)
main.go:15 0x108df38 48c744241002000000 MOVQ $0x2, 0x10(SP)
main.go:15 0x108df41 e85a9dffff CALL fmt.Println(SB)
main.go:16 0x108df46 48c7042400ca9a3b MOVQ $0x3b9aca00, 0(SP)
main.go:16 0x108df4e e87d27fbff CALL time.Sleep(SB)
main.go:15 0x108df53 e979ffffff JMP 0x108ded1
main.go:13 0x108df58 e813dffbff CALL runtime.morestack_noctxt(SB)
main.go:13 0x108df5d e94effffff JMP main.main.func1(SB)
:-1 0x108df62 cc INT $0x3
:-1 0x108df63 cc INT $0x3
:-1 0x108df64 cc INT $0x3
:-1 0x108df65 cc INT $0x3
:-1 0x108df66 cc INT $0x3
:-1 0x108df67 cc INT $0x3
:-1 0x108df68 cc INT $0x3
:-1 0x108df69 cc INT $0x3
:-1 0x108df6a cc INT $0x3
:-1 0x108df6b cc INT $0x3
:-1 0x108df6c cc INT $0x3
:-1 0x108df6d cc INT $0x3
:-1 0x108df6e cc INT $0x3
:-1 0x108df6f cc INT $0x3
顯然,
for {
i += 1
}
直接被優化沒了。我們可以在語句 i += 1
新增一個其他語句來避免被優化掉:
for {
i += 1
time.Sleep(time.Nanosecond)
}
重新執行程式,執行結果“看似正確”了:
╰─➤ go run main.go 1 ↵
i am here 30
i am here 1806937
i am here 3853635
i am here 5485251
...
顯然,如此修改之後,這段程式碼並非真正正確。因為變數 i
存在併發讀寫,即 data race
的問題。而 data race
場景下,go 的行為是未知的。程式設計師最討厭的幾件事中,不確定性必居其一。因此,一步小心寫出 data race
的bug,除錯起來是不太開心的。這裡的例子因為只有幾行程式碼,我們可以目測定位問題。如果程式碼規模比較大,我們可以藉助 golang 工具鏈中的 -race
引數來排查該類問題:
╰─➤ go run -race main.go 2 ↵
==================
WARNING: DATA RACE
Read at 0x0000011d4318 by goroutine 6:
runtime.convT2E64()
/usr/local/go/src/runtime/iface.go:335 +0x0
main.main.func1()
/Users/liudanking/code/golang/gopath/src/test/main.go:15 +0x7d
Previous write at 0x0000011d4318 by main goroutine:
main.main()
/Users/liudanking/code/golang/gopath/src/test/main.go:20 +0x7f
Goroutine 6 (running) created at:
main.main()
/Users/liudanking/code/golang/gopath/src/test/main.go:13 +0x53
==================
i am here 1
i am here 558324
i am here 1075838
除了在 go run
上可以使用 -trace
, 其他幾個常用的golang工具鏈指令也支援這個引數:
$ go test -race mypkg // to test the package
$ go run -race mysrc.go // to run the source file
$ go build -race mycmd // to build the command
$ go install -race mypkg // to install the package
需要說明的是, -trace
並不保證能夠檢查出程式中所有的 data race
, 而檢查出 data race
則必然存在。說起來比較繞,大家記住它跟布隆過濾器 (Bloom Filter) 的真值表是一樣的就對了。
而要把最開始提到的程式碼改對,方法有很多,我們可以使用 The Go Memory Model 推薦的 sync 包中的讀寫鎖即可:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var i = 0
func main() {
runtime.GOMAXPROCS(2)
mtx := sync.RWMutex{}
go func() {
for {
mtx.RLock()
fmt.Println("i am here", i)
mtx.RUnlock()
time.Sleep(time.Second)
}
}()
for {
mtx.Lock()
i += 1
mtx.Unlock()
time.Sleep(time.Nanosecond)
}
擴充套件閱讀
相關文章
- Go語言之讀寫鎖Go
- Go語言中的互斥鎖和讀寫鎖(Mutex和RWMutex)GoMutex
- golang RWMutex讀寫互斥鎖原始碼分析GolangMutex原始碼
- Go語言變數生命期和變數逃逸分析Go變數
- 清華尹成帶你實戰GO案例(19)Go變數Go變數
- 死鎖案例分析
- snap7讀寫PLC變數變數
- GreatSQL 死鎖案例分析SQL
- Go語言之變數逃逸(Escape Analysis)分析Go變數
- 清華尹成帶你實戰GO案例(36)Go 環境變數Go變數
- 常見的 Go 變數縮寫對照表Go變數
- sqlserver查詢時不加鎖SQLServer
- 故障分析 | MySQL死鎖案例分析MySql
- 執行緒間通訊就是讀寫同一個變數執行緒變數
- Java 讀寫鎖 ReentrantReadWriteLock 原始碼分析Java原始碼
- 原始碼分析:升級版的讀寫鎖 StampedLock原始碼
- 原始碼分析:ReentrantReadWriteLock之讀寫鎖原始碼
- linux多執行緒-----同步機制(互斥量、讀寫鎖、條件變數)Linux執行緒變數
- 硬碟預讀引數變化分析硬碟
- 使用 Java 讀寫 JMeter 中的變數JavaJMeter變數
- Java使用讀寫鎖替代同步鎖Java
- golang原始碼分析:sync.Pool 如何從讀寫加鎖到無鎖Golang原始碼
- 非典型開發者的形象三變
- 清華尹成帶你實戰GO案例(43)Go 可變長引數列表Go
- Go 之旅 – 變數Go變數
- Go之旅-變數Go變數
- Go 之旅 - 變數Go變數
- linux多執行緒-----同步物件(互斥量、讀寫鎖、條件變數)的屬性Linux執行緒物件變數
- MySQL MyISAM引擎的讀鎖與寫鎖MySql
- Java讀寫鎖ReadWriteLockJava
- Java中的讀/寫鎖Java
- Java 併發包中的讀寫鎖及其實現分析Java
- MySQL批量更新死鎖案例分析MySql
- 通過String的不變性案例分析Java變數的可變性Java變數
- 淺談Java中的鎖:Synchronized、重入鎖、讀寫鎖Javasynchronized
- 在JS中一個function函式要呼叫另一個函式裡的變數。JSFunction函式變數
- 常見物件-統計大寫,小寫及數字字元的個數案例物件字元
- MySQL鎖問題分析-全域性讀鎖MySql