Go 語(yǔ)言 面向并發(fā)的內(nèi)存模型

2023-03-22 14:57 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch1-basic/ch1-05-mem.html


1.5 面向并發(fā)的內(nèi)存模型

在早期,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)存的。

1.5.1 Goroutine和系統(tǒng)線程

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ā)展。

1.5.2 原子操作

所謂的原子操作就是并發(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è)工作者線程獲取最新的配置信息。所有線程共享配置信息資源。

1.5.3 順序一致性內(nèi)存模型

如果只是想簡(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)志為 truemain 函數(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)順利完成了。

1.5.4 初始化順序

前面函數(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ù)都是在主線程完成,它們也是滿足順序一致性模型的。

1.5.5 Goroutine的創(chuàng)建

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í)行完成后才打印。

1.5.6 基于 Channel 的通信

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)。

1.5.7 不靠譜的同步

前面我們已經(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)題的思路是相同的:使用顯式的同步。



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)