原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html
在早期,CPU 都是以單核的形式順序執(zhí)行機(jī)器指令。Go 語(yǔ)言的祖先 C 語(yǔ)言正是這種順序編程語(yǔ)言的代表。順序編程語(yǔ)言中的順序是指:所有的指令都是以串行的方式執(zhí)行,在相同的時(shí)刻有且僅有一個(gè) CPU 在順序執(zhí)行程序的指令。
隨著處理器技術(shù)的發(fā)展,單核時(shí)代以提升處理器頻率來(lái)提高運(yùn)行效率的方式遇到了瓶頸,目前各種主流的 CPU 頻率基本被鎖定在了 3Ghz 附近。單核 CPU 的發(fā)展的停滯,給多核 CPU 的發(fā)展帶來(lái)了機(jī)遇。相應(yīng)地,編程語(yǔ)言也開始逐步向并行化的方向發(fā)展。Go 語(yǔ)言正是在多核和網(wǎng)絡(luò)化的時(shí)代背景下誕生的原生支持并發(fā)的編程語(yǔ)言。
常見的并行編程有多種模型,主要有多線程、消息傳遞等。從理論上來(lái)看,多線程和基于消息的并發(fā)編程是等價(jià)的。由于多線程并發(fā)模型可以自然對(duì)應(yīng)到多核的處理器,主流的操作系統(tǒng)因此也都提供了系統(tǒng)級(jí)的多線程支持,同時(shí)從概念上講多線程似乎也更直觀,因此多線程編程模型逐步被吸納到主流的編程語(yǔ)言特性或語(yǔ)言擴(kuò)展庫(kù)中。而主流編程語(yǔ)言對(duì)基于消息的并發(fā)編程模型支持則相比較少,Erlang 語(yǔ)言是支持基于消息傳遞并發(fā)編程模型的代表者,它的并發(fā)體之間不共享內(nèi)存。Go 語(yǔ)言是基于消息并發(fā)模型的集大成者,它將基于 CSP 模型的并發(fā)編程內(nèi)置到了語(yǔ)言中,通過(guò)一個(gè) go 關(guān)鍵字就可以輕易地啟動(dòng)一個(gè) Goroutine,與 Erlang 不同的是 Go 語(yǔ)言的 Goroutine 之間是共享內(nèi)存的。
Goroutine是 Go 語(yǔ)言特有的并發(fā)體,是一種輕量級(jí)的線程,由 go 關(guān)鍵字啟動(dòng)。在真實(shí)的 Go 語(yǔ)言的實(shí)現(xiàn)中,goroutine 和系統(tǒng)線程也不是等價(jià)的。盡管兩者的區(qū)別實(shí)際上只是一個(gè)量的區(qū)別,但正是這個(gè)量變引發(fā)了 Go 語(yǔ)言并發(fā)編程質(zhì)的飛躍。
首先,每個(gè)系統(tǒng)級(jí)線程都會(huì)有一個(gè)固定大小的棧(一般默認(rèn)可能是 2MB),這個(gè)棧主要用來(lái)保存函數(shù)遞歸調(diào)用時(shí)參數(shù)和局部變量。固定了棧的大小導(dǎo)致了兩個(gè)問(wèn)題:一是對(duì)于很多只需要很小的棧空間的線程來(lái)說(shuō)是一個(gè)巨大的浪費(fèi),二是對(duì)于少數(shù)需要巨大??臻g的線程來(lái)說(shuō)又面臨棧溢出的風(fēng)險(xiǎn)。針對(duì)這兩個(gè)問(wèn)題的解決方案是:要么降低固定的棧大小,提升空間的利用率;要么增大棧的大小以允許更深的函數(shù)遞歸調(diào)用,但這兩者是沒(méi)法同時(shí)兼得的。相反,一個(gè) Goroutine 會(huì)以一個(gè)很小的棧啟動(dòng)(可能是 2KB 或 4KB),當(dāng)遇到深度遞歸導(dǎo)致當(dāng)前??臻g不足時(shí),Goroutine 會(huì)根據(jù)需要?jiǎng)討B(tài)地伸縮棧的大?。ㄖ髁鲗?shí)現(xiàn)中棧的最大值可達(dá)到1GB)。因?yàn)閱?dòng)的代價(jià)很小,所以我們可以輕易地啟動(dòng)成千上萬(wàn)個(gè) Goroutine。
Go的運(yùn)行時(shí)還包含了其自己的調(diào)度器,這個(gè)調(diào)度器使用了一些技術(shù)手段,可以在 n 個(gè)操作系統(tǒng)線程上多工調(diào)度 m 個(gè) Goroutine。Go 調(diào)度器的工作和內(nèi)核的調(diào)度是相似的,但是這個(gè)調(diào)度器只關(guān)注單獨(dú)的 Go 程序中的 Goroutine。Goroutine 采用的是半搶占式的協(xié)作調(diào)度,只有在當(dāng)前 Goroutine 發(fā)生阻塞時(shí)才會(huì)導(dǎo)致調(diào)度;同時(shí)發(fā)生在用戶態(tài),調(diào)度器會(huì)根據(jù)具體函數(shù)只保存必要的寄存器,切換的代價(jià)要比系統(tǒng)線程低得多。運(yùn)行時(shí)有一個(gè) runtime.GOMAXPROCS
變量,用于控制當(dāng)前運(yùn)行正常非阻塞
Goroutine 的系統(tǒng)線程數(shù)目。
在 Go 語(yǔ)言中啟動(dòng)一個(gè) Goroutine 不僅和調(diào)用函數(shù)一樣簡(jiǎn)單,而且 Goroutine 之間調(diào)度代價(jià)也很低,這些因素極大地促進(jìn)了并發(fā)編程的流行和發(fā)展。
所謂的原子操作就是并發(fā)編程中“最小的且不可并行化”的操作。通常,如果多個(gè)并發(fā)體對(duì)同一個(gè)共享資源進(jìn)行的操作是原子的話,那么同一時(shí)刻最多只能有一個(gè)并發(fā)體對(duì)該資源進(jìn)行操作。從線程角度看,在當(dāng)前線程修改共享資源期間,其它的線程是不能訪問(wèn)該資源的。原子操作對(duì)于多線程并發(fā)編程模型來(lái)說(shuō),不會(huì)發(fā)生有別于單線程的意外情況,共享資源的完整性可以得到保證。
一般情況下,原子操作都是通過(guò)“互斥”訪問(wèn)來(lái)保證的,通常由特殊的 CPU 指令提供保護(hù)。當(dāng)然,如果僅僅是想模擬下粗粒度的原子操作,我們可以借助于 sync.Mutex
來(lái)實(shí)現(xiàn):
import (
"sync"
)
var total struct {
sync.Mutex
value int
}
func worker(wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i <= 100; i++ {
total.Lock()
total.value += i
total.Unlock()
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg)
go worker(&wg)
wg.Wait()
fmt.Println(total.value)
}
在 worker
的循環(huán)中,為了保證 total.value += i
的原子性,我們通過(guò) sync.Mutex
加鎖和解鎖來(lái)保證該語(yǔ)句在同一時(shí)刻只被一個(gè)線程訪問(wèn)。對(duì)于多線程模型的程序而言,進(jìn)出臨界區(qū)前后進(jìn)行加鎖和解鎖都是必須的。如果沒(méi)有鎖的保護(hù),total
的最終值將由于多線程之間的競(jìng)爭(zhēng)而可能會(huì)不正確。
用互斥鎖來(lái)保護(hù)一個(gè)數(shù)值型的共享資源,麻煩且效率低下。標(biāo)準(zhǔn)庫(kù)的 sync/atomic
包對(duì)原子操作提供了豐富的支持。我們可以重新實(shí)現(xiàn)上面的例子:
import (
"sync"
"sync/atomic"
)
var total uint64
func worker(wg *sync.WaitGroup) {
defer wg.Done()
var i uint64
for i = 0; i <= 100; i++ {
atomic.AddUint64(&total, i)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg)
go worker(&wg)
wg.Wait()
}
atomic.AddUint64
函數(shù)調(diào)用保證了 total
的讀取、更新和保存是一個(gè)原子操作,因此在多線程中訪問(wèn)也是安全的。
原子操作配合互斥鎖可以實(shí)現(xiàn)非常高效的單件模式?;コ怄i的代價(jià)比普通整數(shù)的原子讀寫高很多,在性能敏感的地方可以增加一個(gè)數(shù)字型的標(biāo)志位,通過(guò)原子檢測(cè)標(biāo)志位狀態(tài)降低互斥鎖的使用次數(shù)來(lái)提高性能。
type singleton struct {}
var (
instance *singleton
initialized uint32
mu sync.Mutex
)
func Instance() *singleton {
if atomic.LoadUint32(&initialized) == 1 {
return instance
}
mu.Lock()
defer mu.Unlock()
if instance == nil {
defer atomic.StoreUint32(&initialized, 1)
instance = &singleton{}
}
return instance
}
我們可以將通用的代碼提取出來(lái),就成了標(biāo)準(zhǔn)庫(kù)中 sync.Once
的實(shí)現(xiàn):
type Once struct {
m Mutex
done uint32
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
基于 sync.Once
重新實(shí)現(xiàn)單件模式:
var (
instance *singleton
once sync.Once
)
func Instance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
sync/atomic
包對(duì)基本的數(shù)值類型及復(fù)雜對(duì)象的讀寫都提供了原子操作的支持。atomic.Value
原子對(duì)象提供了 Load
和 Store
兩個(gè)原子方法,分別用于加載和保存數(shù)據(jù),返回值和參數(shù)都是 interface{}
類型,因此可以用于任意的自定義復(fù)雜類型。
var config atomic.Value // 保存當(dāng)前配置信息
// 初始化配置信息
config.Store(loadConfig())
// 啟動(dòng)一個(gè)后臺(tái)線程, 加載更新后的配置信息
go func() {
for {
time.Sleep(time.Second)
config.Store(loadConfig())
}
}()
// 用于處理請(qǐng)求的工作者線程始終采用最新的配置信息
for i := 0; i < 10; i++ {
go func() {
for r := range requests() {
c := config.Load()
// ...
}
}()
}
這是一個(gè)簡(jiǎn)化的生產(chǎn)者消費(fèi)者模型:后臺(tái)線程生成最新的配置信息;前臺(tái)多個(gè)工作者線程獲取最新的配置信息。所有線程共享配置信息資源。
如果只是想簡(jiǎn)單地在線程之間進(jìn)行數(shù)據(jù)同步的話,原子操作已經(jīng)為編程人員提供了一些同步保障。不過(guò)這種保障有一個(gè)前提:順序一致性的內(nèi)存模型。要了解順序一致性,我們先看看一個(gè)簡(jiǎn)單的例子:
var a string
var done bool
func setup() {
a = "hello, world"
done = true
}
func main() {
go setup()
for !done {}
print(a)
}
我們創(chuàng)建了 setup
線程,用于對(duì)字符串 a
的初始化工作,初始化完成之后設(shè)置 done
標(biāo)志為 true
。main
函數(shù)所在的主線程中,通過(guò) for !done {}
檢測(cè) done
變?yōu)?nbsp;true
時(shí),認(rèn)為字符串初始化工作完成,然后進(jìn)行字符串的打印工作。
但是 Go 語(yǔ)言并不保證在 main
函數(shù)中觀測(cè)到的對(duì) done
的寫入操作發(fā)生在對(duì)字符串 a
的寫入的操作之后,因此程序很可能打印一個(gè)空字符串。更糟糕的是,因?yàn)閮蓚€(gè)線程之間沒(méi)有同步事件,setup
線程對(duì) done
的寫入操作甚至無(wú)法被 main
線程看到,main
函數(shù)有可能陷入死循環(huán)中。
在 Go 語(yǔ)言中,同一個(gè) Goroutine 線程內(nèi)部,順序一致性內(nèi)存模型是得到保證的。但是不同的 Goroutine 之間,并不滿足順序一致性內(nèi)存模型,需要通過(guò)明確定義的同步事件來(lái)作為同步的參考。如果兩個(gè)事件不可排序,那么就說(shuō)這兩個(gè)事件是并發(fā)的。為了最大化并行,Go 語(yǔ)言的編譯器和處理器在不影響上述規(guī)定的前提下可能會(huì)對(duì)執(zhí)行語(yǔ)句重新排序(CPU 也會(huì)對(duì)一些指令進(jìn)行亂序執(zhí)行)。
因此,如果在一個(gè) Goroutine 中順序執(zhí)行 a = 1; b = 2;
兩個(gè)語(yǔ)句,雖然在當(dāng)前的 Goroutine 中可以認(rèn)為 a = 1;
語(yǔ)句先于 b = 2;
語(yǔ)句執(zhí)行,但是在另一個(gè) Goroutine 中 b = 2;
語(yǔ)句可能會(huì)先于 a = 1;
語(yǔ)句執(zhí)行,甚至在另一個(gè)
Goroutine 中無(wú)法看到它們的變化(可能始終在寄存器中)。也就是說(shuō)在另一個(gè) Goroutine 看來(lái), a = 1; b = 2;
兩個(gè)語(yǔ)句的執(zhí)行順序是不確定的。如果一個(gè)并發(fā)程序無(wú)法確定事件的順序關(guān)系,那么程序的運(yùn)行結(jié)果往往會(huì)有不確定的結(jié)果。比如下面這個(gè)程序:
func main() {
go println("你好, 世界")
}
根據(jù) Go 語(yǔ)言規(guī)范,main
函數(shù)退出時(shí)程序結(jié)束,不會(huì)等待任何后臺(tái)線程。因?yàn)?Goroutine 的執(zhí)行和 main
函數(shù)的返回事件是并發(fā)的,誰(shuí)都有可能先發(fā)生,所以什么時(shí)候打印,能否打印都是未知的。
用前面的原子操作并不能解決問(wèn)題,因?yàn)槲覀儫o(wú)法確定兩個(gè)原子操作之間的順序。解決問(wèn)題的辦法就是通過(guò)同步原語(yǔ)來(lái)給兩個(gè)事件明確排序:
func main() {
done := make(chan int)
go func(){
println("你好, 世界")
done <- 1
}()
<-done
}
當(dāng) <-done
執(zhí)行時(shí),必然要求 done <- 1
也已經(jīng)執(zhí)行。根據(jù)同一個(gè) Goroutine 依然滿足順序一致性規(guī)則,我們可以判斷當(dāng) done <- 1
執(zhí)行時(shí),println("你好, 世界")
語(yǔ)句必然已經(jīng)執(zhí)行完成了。因此,現(xiàn)在的程序確??梢哉4蛴〗Y(jié)果。
當(dāng)然,通過(guò) sync.Mutex
互斥量也是可以實(shí)現(xiàn)同步的:
func main() {
var mu sync.Mutex
mu.Lock()
go func(){
println("你好, 世界")
mu.Unlock()
}()
mu.Lock()
}
可以確定后臺(tái)線程的 mu.Unlock()
必然在 println("你好, 世界")
完成后發(fā)生(同一個(gè)線程滿足順序一致性),main
函數(shù)的第二個(gè) mu.Lock()
必然在后臺(tái)線程的 mu.Unlock()
之后發(fā)生(sync.Mutex
保證),此時(shí)后臺(tái)線程的打印工作已經(jīng)順利完成了。
前面函數(shù)章節(jié)中我們已經(jīng)簡(jiǎn)單介紹過(guò)程序的初始化順序,這是屬于 Go 語(yǔ)言面向并發(fā)的內(nèi)存模型的基礎(chǔ)規(guī)范。
Go程序的初始化和執(zhí)行總是從 main.main
函數(shù)開始的。但是如果 main
包里導(dǎo)入了其它的包,則會(huì)按照順序?qū)⑺鼈儼M(jìn) main
包里(這里的導(dǎo)入順序依賴具體實(shí)現(xiàn),一般可能是以文件名或包路徑名的字符串順序?qū)耄?。如果某個(gè)包被多次導(dǎo)入的話,在執(zhí)行的時(shí)候只會(huì)導(dǎo)入一次。當(dāng)一個(gè)包被導(dǎo)入時(shí),如果它還導(dǎo)入了其它的包,則先將其它的包包含進(jìn)來(lái),然后創(chuàng)建和初始化這個(gè)包的常量和變量。然后就是調(diào)用包里的 init
函數(shù),如果一個(gè)包有多個(gè) init
函數(shù)的話,實(shí)現(xiàn)可能是以文件名的順序調(diào)用,同一個(gè)文件內(nèi)的多個(gè) init
則是以出現(xiàn)的順序依次調(diào)用(init
不是普通函數(shù),可以定義有多個(gè),所以不能被其它函數(shù)調(diào)用)。最終,在 main
包的所有包常量、包變量被創(chuàng)建和初始化,并且 init
函數(shù)被執(zhí)行后,才會(huì)進(jìn)入 main.main
函數(shù),程序開始正常執(zhí)行。下圖是
Go 程序函數(shù)啟動(dòng)順序的示意圖:
圖 1-12 包初始化流程
要注意的是,在 main.main
函數(shù)執(zhí)行之前所有代碼都運(yùn)行在同一個(gè) Goroutine 中,也是運(yùn)行在程序的主系統(tǒng)線程中。如果某個(gè) init
函數(shù)內(nèi)部用 go 關(guān)鍵字啟動(dòng)了新的 Goroutine 的話,新的 Goroutine 和 main.main
函數(shù)是并發(fā)執(zhí)行的。
因?yàn)樗械?nbsp;init
函數(shù)和 main
函數(shù)都是在主線程完成,它們也是滿足順序一致性模型的。
go
語(yǔ)句會(huì)在當(dāng)前 Goroutine 對(duì)應(yīng)函數(shù)返回前創(chuàng)建新的 Goroutine。例如:
var a string
func f() {
print(a)
}
func hello() {
a = "hello, world"
go f()
}
執(zhí)行 go f()
語(yǔ)句創(chuàng)建 Goroutine 和 hello
函數(shù)是在同一個(gè) Goroutine 中執(zhí)行, 根據(jù)語(yǔ)句的書寫順序可以確定 Goroutine 的創(chuàng)建發(fā)生在 hello
函數(shù)返回之前, 但是新創(chuàng)建 Goroutine 對(duì)應(yīng)的 f()
的執(zhí)行事件和 hello
函數(shù)返回的事件則是不可排序的,也就是并發(fā)的。調(diào)用 hello
可能會(huì)在將來(lái)的某一時(shí)刻打印 "hello, world"
,也很可能是在 hello
函數(shù)執(zhí)行完成后才打印。
Channel 通信是在 Goroutine 之間進(jìn)行同步的主要方法。在無(wú)緩存的 Channel 上的每一次發(fā)送操作都有與其對(duì)應(yīng)的接收操作相配對(duì),發(fā)送和接收操作通常發(fā)生在不同的 Goroutine 上(在同一個(gè) Goroutine 上執(zhí)行兩個(gè)操作很容易導(dǎo)致死鎖)。無(wú)緩存的 Channel 上的發(fā)送操作總在對(duì)應(yīng)的接收操作完成前發(fā)生.
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "你好, 世界"
done <- true
}
func main() {
go aGoroutine()
<-done
println(msg)
}
可保證打印出“你好, 世界”。該程序首先對(duì) msg
進(jìn)行寫入,然后在 done
管道上發(fā)送同步信號(hào),隨后從 done
接收對(duì)應(yīng)的同步信號(hào),最后執(zhí)行 println
函數(shù)。
若在關(guān)閉 Channel 后繼續(xù)從中接收數(shù)據(jù),接收者就會(huì)收到該 Channel 返回的零值。因此在這個(gè)例子中,用 close(c)
關(guān)閉管道代替 done <- false
依然能保證該程序產(chǎn)生相同的行為。
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "你好, 世界"
close(done)
}
func main() {
go aGoroutine()
<-done
println(msg)
}
對(duì)于從無(wú)緩沖 Channel 進(jìn)行的接收,發(fā)生在對(duì)該 Channel 進(jìn)行的發(fā)送完成之前。
基于上面這個(gè)規(guī)則可知,交換兩個(gè) Goroutine 中的接收和發(fā)送操作也是可以的(但是很危險(xiǎn)):
var done = make(chan bool)
var msg string
func aGoroutine() {
msg = "hello, world"
<-done
}
func main() {
go aGoroutine()
done <- true
println(msg)
}
也可保證打印出“hello, world”。因?yàn)?nbsp;main
線程中 done <- true
發(fā)送完成前,后臺(tái)線程 <-done
接收已經(jīng)開始,這保證 msg = "hello, world"
被執(zhí)行了,所以之后 println(msg)
的msg已經(jīng)被賦值過(guò)了。簡(jiǎn)而言之,后臺(tái)線程首先對(duì) msg
進(jìn)行寫入,然后從 done
中接收信號(hào),隨后 main
線程向 done
發(fā)送對(duì)應(yīng)的信號(hào),最后執(zhí)行 println
函數(shù)完成。但是,若該
Channel 為帶緩沖的(例如,done = make(chan bool, 1)
),main
線程的 done <- true
接收操作將不會(huì)被后臺(tái)線程的 <-done
接收操作阻塞,該程序?qū)o(wú)法保證打印出“hello, world”。
對(duì)于帶緩沖的Channel,對(duì)于 Channel 的第 K
個(gè)接收完成操作發(fā)生在第 K+C
個(gè)發(fā)送操作完成之前,其中 C
是 Channel 的緩存大小。 如果將 C
設(shè)置為 0 自然就對(duì)應(yīng)無(wú)緩存的 Channel,也即使第 K 個(gè)接收完成在第 K 個(gè)發(fā)送完成之前。因?yàn)闊o(wú)緩存的
Channel 只能同步發(fā) 1 個(gè),也就簡(jiǎn)化為前面無(wú)緩存 Channel 的規(guī)則:對(duì)于從無(wú)緩沖 Channel 進(jìn)行的接收,發(fā)生在對(duì)該 Channel 進(jìn)行的發(fā)送完成之前。
我們可以根據(jù)控制 Channel 的緩存大小來(lái)控制并發(fā)執(zhí)行的 Goroutine 的最大數(shù)目, 例如:
var limit = make(chan int, 3)
var work = []func(){
func() { println("1"); time.Sleep(1 * time.Second) },
func() { println("2"); time.Sleep(1 * time.Second) },
func() { println("3"); time.Sleep(1 * time.Second) },
func() { println("4"); time.Sleep(1 * time.Second) },
func() { println("5"); time.Sleep(1 * time.Second) },
}
func main() {
for _, w := range work {
go func(w func()) {
limit <- 1
w()
<-limit
}(w)
}
select{}
}
在循環(huán)創(chuàng)建 Goroutine
過(guò)程中,使用了匿名函數(shù)并在函數(shù)中引用了循環(huán)變量 w
,由于 w
是引用傳遞的而非值傳遞,因此無(wú)法保證 Goroutine
在運(yùn)行時(shí)調(diào)用的 w
與循環(huán)創(chuàng)建時(shí)的 w
是同一個(gè)值,為了解決這個(gè)問(wèn)題,我們可以利用函數(shù)傳參的值復(fù)制來(lái)為每個(gè) Goroutine
單獨(dú)復(fù)制一份 w
。
循環(huán)創(chuàng)建結(jié)束后,在 main
函數(shù)中最后一句 select{}
是一個(gè)空的管道選擇語(yǔ)句,該語(yǔ)句會(huì)導(dǎo)致 main
線程阻塞,從而避免程序過(guò)早退出。還有 for{}
、<-make(chan int)
等諸多方法可以達(dá)到類似的效果。因?yàn)?nbsp;main
線程被阻塞了,如果需要程序正常退出的話可以通過(guò)調(diào)用 os.Exit(0)
實(shí)現(xiàn)。
前面我們已經(jīng)分析過(guò),下面代碼無(wú)法保證正常打印結(jié)果。實(shí)際的運(yùn)行效果也是大概率不能正常輸出結(jié)果。
func main() {
go println("你好, 世界")
}
剛接觸 Go 語(yǔ)言的話,可能希望通過(guò)加入一個(gè)隨機(jī)的休眠時(shí)間來(lái)保證正常的輸出:
func main() {
go println("hello, world")
time.Sleep(time.Second)
}
因?yàn)橹骶€程休眠了 1 秒鐘,因此這個(gè)程序大概率是可以正常輸出結(jié)果的。因此,很多人會(huì)覺(jué)得這個(gè)程序已經(jīng)沒(méi)有問(wèn)題了。但是這個(gè)程序是不穩(wěn)健的,依然有失敗的可能性。我們先假設(shè)程序是可以穩(wěn)定輸出結(jié)果的。因?yàn)?Go 線程的啟動(dòng)是非阻塞的,main
線程顯式休眠了 1 秒鐘退出導(dǎo)致程序結(jié)束,我們可以近似地認(rèn)為程序總共執(zhí)行了 1 秒多時(shí)間。現(xiàn)在假設(shè) println
函數(shù)內(nèi)部實(shí)現(xiàn)休眠的時(shí)間大于 main
線程休眠的時(shí)間的話,就會(huì)導(dǎo)致矛盾:后臺(tái)線程既然先于 main
線程完成打印,那么執(zhí)行時(shí)間肯定是小于 main
線程執(zhí)行時(shí)間的。當(dāng)然這是不可能的。
嚴(yán)謹(jǐn)?shù)牟l(fā)程序的正確性不應(yīng)該是依賴于 CPU 的執(zhí)行速度和休眠時(shí)間等不靠譜的因素的。嚴(yán)謹(jǐn)?shù)牟l(fā)也應(yīng)該是可以靜態(tài)推導(dǎo)出結(jié)果的:根據(jù)線程內(nèi)順序一致性,結(jié)合 Channel 或 sync
同步事件的可排序性來(lái)推導(dǎo),最終完成各個(gè)線程各段代碼的偏序關(guān)系排序。如果兩個(gè)事件無(wú)法根據(jù)此規(guī)則來(lái)排序,那么它們就是并發(fā)的,也就是執(zhí)行先后順序不可靠的。
解決同步問(wèn)題的思路是相同的:使用顯式的同步。
![]() | ![]() |
更多建議: