- Go語言高級編程(第2版)
- 柴樹杉 曹春暉
- 1059字
- 2025-08-07 17:56:15
1.5.3 順序一致性內存模型
如果只是想簡單地在線程之間進行數據同步的話,原子操作已經為編程人員提供了一些同步保障。不過這種保障有一個前提:順序一致性內存模型。要了解順序一致性,先看一個簡單的例子:
var a string var done bool func setup() { a = "hello, world" done = true } func main() { go setup() for !done {} print(a) }
我們創建了setup
線程,用于對字符串a
的初始化工作,初始化完成之后設置done
標志為true
。main()
函數所在的主線程中,通過for !done {}
檢測done
變為true
時,認為字符串初始化工作完成,然后進行字符串的打印工?作。
但是,Go語言并不保證在main()
函數中觀測到的對done
的寫入操作發生在對字符串a
的寫入操作之后,因此程序很可能打印一個空字符串。更糟糕的是,因為兩個線程之間沒有同步事件,setup
線程對done
的寫入操作甚至無法被main
線程看到,main()
函數有可能陷入死循環?中。
在Go語言中,同一個goroutine內部,順序一致性內存模型是得到保證的。但是不同的goroutine之間,并不滿足順序一致性內存模型,需要通過明確定義的同步事件來作為同步的參考。如果兩個事件不可排序,那么就說這兩個事件是并發的。為了最大化并行,Go語言的編譯器和處理器在不影響上述規定的前提下可能會對執行語句重新排序(CPU也會對一些指令進行亂序執行)。
因此,如果在一個goroutine中順序執行a = 1;
和b = 2;
這
兩個語句,雖然在當前的goroutine中可以認為a = 1;
語句先于b = 2;
語句執行,但是在另一個goroutine中b = 2;
語句可能會先于a = 1;
語句執行,甚至無法看到它們的變化(可能始終在寄存器中)。也就是說,在另一個goroutine看來,a = 1;
和b = 2;
這
兩個語句的執行順序是不確定的。如果一個并發程序無法確定事件的順序關系,那么程序的運行結果往往會不確定。例如,下面這個程序:
func main() { go println("你好, 世界") }
根據Go語言規范,main()
函數退出時程序結束,不會等待任何后臺線程。但因為goroutine的執行和main()
函數的返回事件是并發的,誰都有可能先發生,所以什么時候打印、能否打印都是未知?的。
用前面的原子操作并不能解決問題,因為我們無法確定兩個原子操作之間的順序。解決問題的辦法就是通過同步原語來給兩個事件明確排序:
func main() { done := make(chan int) go func(){ println("你好, 世界") done <- 1 }() <-done }
當<-done
執行時,必然要求done <- 1
也已經執行。根據同一個goroutine依然滿足順序一致性規則,可以判斷當done <- 1
執行時,println
("
你好
,
世界
")
語句必然已經執行完成了。因此,現在的程序確??梢哉4蛴〗Y?果。
當然,通過sync.Mutex
互斥量也是可以實現同步的:
func main() { var mu sync.Mutex mu.Lock() go func(){ println("你好, 世界") mu.Unlock() }() mu.Lock() }
可以確定,后臺線程的mu.Unlock()
必然在println
("
你好
,
世界
")
完成后發生(同一個線程滿足順序一致性),main()
函數的第二個mu.Lock()
必然在后臺線程的mu.Unlock()
之后發生(sync.Mutex
保證),此時后臺線程的打印工作已經順利完成?了。