Go 語(yǔ)言 Channels

2023-03-14 16:57 更新

原文鏈接:https://gopl-zh.github.io/ch8/ch8-04.html


8.4. Channels

如果說(shuō)goroutine是Go語(yǔ)言程序的并發(fā)體的話(huà),那么channels則是它們之間的通信機(jī)制。一個(gè)channel是一個(gè)通信機(jī)制,它可以讓一個(gè)goroutine通過(guò)它給另一個(gè)goroutine發(fā)送值信息。每個(gè)channel都有一個(gè)特殊的類(lèi)型,也就是channels可發(fā)送數(shù)據(jù)的類(lèi)型。一個(gè)可以發(fā)送int類(lèi)型數(shù)據(jù)的channel一般寫(xiě)為chan int。

使用內(nèi)置的make函數(shù),我們可以創(chuàng)建一個(gè)channel:

ch := make(chan int) // ch has type 'chan int'

和map類(lèi)似,channel也對(duì)應(yīng)一個(gè)make創(chuàng)建的底層數(shù)據(jù)結(jié)構(gòu)的引用。當(dāng)我們復(fù)制一個(gè)channel或用于函數(shù)參數(shù)傳遞時(shí),我們只是拷貝了一個(gè)channel引用,因此調(diào)用者和被調(diào)用者將引用同一個(gè)channel對(duì)象。和其它的引用類(lèi)型一樣,channel的零值也是nil。

兩個(gè)相同類(lèi)型的channel可以使用==運(yùn)算符比較。如果兩個(gè)channel引用的是相同的對(duì)象,那么比較的結(jié)果為真。一個(gè)channel也可以和nil進(jìn)行比較。

一個(gè)channel有發(fā)送和接受兩個(gè)主要操作,都是通信行為。一個(gè)發(fā)送語(yǔ)句將一個(gè)值從一個(gè)goroutine通過(guò)channel發(fā)送到另一個(gè)執(zhí)行接收操作的goroutine。發(fā)送和接收兩個(gè)操作都使用<-運(yùn)算符。在發(fā)送語(yǔ)句中,<-運(yùn)算符分割channel和要發(fā)送的值。在接收語(yǔ)句中,<-運(yùn)算符寫(xiě)在channel對(duì)象之前。一個(gè)不使用接收結(jié)果的接收操作也是合法的。

ch <- x  // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch     // a receive statement; result is discarded

Channel還支持close操作,用于關(guān)閉channel,隨后對(duì)基于該channel的任何發(fā)送操作都將導(dǎo)致panic異常。對(duì)一個(gè)已經(jīng)被close過(guò)的channel進(jìn)行接收操作依然可以接受到之前已經(jīng)成功發(fā)送的數(shù)據(jù);如果channel中已經(jīng)沒(méi)有數(shù)據(jù)的話(huà)將產(chǎn)生一個(gè)零值的數(shù)據(jù)。

使用內(nèi)置的close函數(shù)就可以關(guān)閉一個(gè)channel:

close(ch)

以最簡(jiǎn)單方式調(diào)用make函數(shù)創(chuàng)建的是一個(gè)無(wú)緩存的channel,但是我們也可以指定第二個(gè)整型參數(shù),對(duì)應(yīng)channel的容量。如果channel的容量大于零,那么該channel就是帶緩存的channel。

ch = make(chan int)    // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

我們將先討論無(wú)緩存的channel,然后在8.4.4節(jié)討論帶緩存的channel。

8.4.1. 不帶緩存的Channels

一個(gè)基于無(wú)緩存Channels的發(fā)送操作將導(dǎo)致發(fā)送者goroutine阻塞,直到另一個(gè)goroutine在相同的Channels上執(zhí)行接收操作,當(dāng)發(fā)送的值通過(guò)Channels成功傳輸之后,兩個(gè)goroutine可以繼續(xù)執(zhí)行后面的語(yǔ)句。反之,如果接收操作先發(fā)生,那么接收者goroutine也將阻塞,直到有另一個(gè)goroutine在相同的Channels上執(zhí)行發(fā)送操作。

基于無(wú)緩存Channels的發(fā)送和接收操作將導(dǎo)致兩個(gè)goroutine做一次同步操作。因?yàn)檫@個(gè)原因,無(wú)緩存Channels有時(shí)候也被稱(chēng)為同步Channels。當(dāng)通過(guò)一個(gè)無(wú)緩存Channels發(fā)送數(shù)據(jù)時(shí),接收者收到數(shù)據(jù)發(fā)生在再次喚醒發(fā)送者goroutine之前(譯注:happens before,這是Go語(yǔ)言并發(fā)內(nèi)存模型的一個(gè)關(guān)鍵術(shù)語(yǔ)!)。

在討論并發(fā)編程時(shí),當(dāng)我們說(shuō)x事件在y事件之前發(fā)生(happens before),我們并不是說(shuō)x事件在時(shí)間上比y時(shí)間更早;我們要表達(dá)的意思是要保證在此之前的事件都已經(jīng)完成了,例如在此之前的更新某些變量的操作已經(jīng)完成,你可以放心依賴(lài)這些已完成的事件了。

當(dāng)我們說(shuō)x事件既不是在y事件之前發(fā)生也不是在y事件之后發(fā)生,我們就說(shuō)x事件和y事件是并發(fā)的。這并不是意味著x事件和y事件就一定是同時(shí)發(fā)生的,我們只是不能確定這兩個(gè)事件發(fā)生的先后順序。在下一章中我們將看到,當(dāng)兩個(gè)goroutine并發(fā)訪(fǎng)問(wèn)了相同的變量時(shí),我們有必要保證某些事件的執(zhí)行順序,以避免出現(xiàn)某些并發(fā)問(wèn)題。

在8.3節(jié)的客戶(hù)端程序,它在主goroutine中(譯注:就是執(zhí)行main函數(shù)的goroutine)將標(biāo)準(zhǔn)輸入復(fù)制到server,因此當(dāng)客戶(hù)端程序關(guān)閉標(biāo)準(zhǔn)輸入時(shí),后臺(tái)goroutine可能依然在工作。我們需要讓主goroutine等待后臺(tái)goroutine完成工作后再退出,我們使用了一個(gè)channel來(lái)同步兩個(gè)goroutine:

gopl.io/ch8/netcat3

func main() {
    conn, err := net.Dial("tcp", "localhost:8000")
    if err != nil {
        log.Fatal(err)
    }
    done := make(chan struct{})
    go func() {
        io.Copy(os.Stdout, conn) // NOTE: ignoring errors
        log.Println("done")
        done <- struct{}{} // signal the main goroutine
    }()
    mustCopy(conn, os.Stdin)
    conn.Close()
    <-done // wait for background goroutine to finish
}

當(dāng)用戶(hù)關(guān)閉了標(biāo)準(zhǔn)輸入,主goroutine中的mustCopy函數(shù)調(diào)用將返回,然后調(diào)用conn.Close()關(guān)閉讀和寫(xiě)方向的網(wǎng)絡(luò)連接。關(guān)閉網(wǎng)絡(luò)連接中的寫(xiě)方向的連接將導(dǎo)致server程序收到一個(gè)文件(end-of-file)結(jié)束的信號(hào)。關(guān)閉網(wǎng)絡(luò)連接中讀方向的連接將導(dǎo)致后臺(tái)goroutine的io.Copy函數(shù)調(diào)用返回一個(gè)“read from closed connection”(“從關(guān)閉的連接讀”)類(lèi)似的錯(cuò)誤,因此我們臨時(shí)移除了錯(cuò)誤日志語(yǔ)句;在練習(xí)8.3將會(huì)提供一個(gè)更好的解決方案。(需要注意的是go語(yǔ)句調(diào)用了一個(gè)函數(shù)字面量,這是Go語(yǔ)言中啟動(dòng)goroutine常用的形式。)

在后臺(tái)goroutine返回之前,它先打印一個(gè)日志信息,然后向done對(duì)應(yīng)的channel發(fā)送一個(gè)值。主goroutine在退出前先等待從done對(duì)應(yīng)的channel接收一個(gè)值。因此,總是可以在程序退出前正確輸出“done”消息。

基于channels發(fā)送消息有兩個(gè)重要方面。首先每個(gè)消息都有一個(gè)值,但是有時(shí)候通訊的事實(shí)和發(fā)生的時(shí)刻也同樣重要。當(dāng)我們更希望強(qiáng)調(diào)通訊發(fā)生的時(shí)刻時(shí),我們將它稱(chēng)為消息事件。有些消息事件并不攜帶額外的信息,它僅僅是用作兩個(gè)goroutine之間的同步,這時(shí)候我們可以用struct{}空結(jié)構(gòu)體作為channels元素的類(lèi)型,雖然也可以使用bool或int類(lèi)型實(shí)現(xiàn)同樣的功能,done <- 1語(yǔ)句也比done <- struct{}{}更短。

練習(xí) 8.3: 在netcat3例子中,conn雖然是一個(gè)interface類(lèi)型的值,但是其底層真實(shí)類(lèi)型是*net.TCPConn,代表一個(gè)TCP連接。一個(gè)TCP連接有讀和寫(xiě)兩個(gè)部分,可以使用CloseRead和CloseWrite方法分別關(guān)閉它們。修改netcat3的主goroutine代碼,只關(guān)閉網(wǎng)絡(luò)連接中寫(xiě)的部分,這樣的話(huà)后臺(tái)goroutine可以在標(biāo)準(zhǔn)輸入被關(guān)閉后繼續(xù)打印從reverb1服務(wù)器傳回的數(shù)據(jù)。(要在reverb2服務(wù)器也完成同樣的功能是比較困難的;參考練習(xí) 8.4。)

8.4.2. 串聯(lián)的Channels(Pipeline)

Channels也可以用于將多個(gè)goroutine連接在一起,一個(gè)Channel的輸出作為下一個(gè)Channel的輸入。這種串聯(lián)的Channels就是所謂的管道(pipeline)。下面的程序用兩個(gè)channels將三個(gè)goroutine串聯(lián)起來(lái),如圖8.1所示。


第一個(gè)goroutine是一個(gè)計(jì)數(shù)器,用于生成0、1、2、……形式的整數(shù)序列,然后通過(guò)channel將該整數(shù)序列發(fā)送給第二個(gè)goroutine;第二個(gè)goroutine是一個(gè)求平方的程序,對(duì)收到的每個(gè)整數(shù)求平方,然后將平方后的結(jié)果通過(guò)第二個(gè)channel發(fā)送給第三個(gè)goroutine;第三個(gè)goroutine是一個(gè)打印程序,打印收到的每個(gè)整數(shù)。為了保持例子清晰,我們有意選擇了非常簡(jiǎn)單的函數(shù),當(dāng)然三個(gè)goroutine的計(jì)算很簡(jiǎn)單,在現(xiàn)實(shí)中確實(shí)沒(méi)有必要為如此簡(jiǎn)單的運(yùn)算構(gòu)建三個(gè)goroutine。

gopl.io/ch8/pipeline1

func main() {
    naturals := make(chan int)
    squares := make(chan int)

    // Counter
    go func() {
        for x := 0; ; x++ {
            naturals <- x
        }
    }()

    // Squarer
    go func() {
        for {
            x := <-naturals
            squares <- x * x
        }
    }()

    // Printer (in main goroutine)
    for {
        fmt.Println(<-squares)
    }
}

如您所料,上面的程序?qū)⑸?、1、4、9、……形式的無(wú)窮數(shù)列。像這樣的串聯(lián)Channels的管道(Pipelines)可以用在需要長(zhǎng)時(shí)間運(yùn)行的服務(wù)中,每個(gè)長(zhǎng)時(shí)間運(yùn)行的goroutine可能會(huì)包含一個(gè)死循環(huán),在不同goroutine的死循環(huán)內(nèi)部使用串聯(lián)的Channels來(lái)通信。但是,如果我們希望通過(guò)Channels只發(fā)送有限的數(shù)列該如何處理呢?

如果發(fā)送者知道,沒(méi)有更多的值需要發(fā)送到channel的話(huà),那么讓接收者也能及時(shí)知道沒(méi)有多余的值可接收將是有用的,因?yàn)榻邮照呖梢酝V共槐匾慕邮盏却?。這可以通過(guò)內(nèi)置的close函數(shù)來(lái)關(guān)閉channel實(shí)現(xiàn):

close(naturals)

當(dāng)一個(gè)channel被關(guān)閉后,再向該channel發(fā)送數(shù)據(jù)將導(dǎo)致panic異常。當(dāng)一個(gè)被關(guān)閉的channel中已經(jīng)發(fā)送的數(shù)據(jù)都被成功接收后,后續(xù)的接收操作將不再阻塞,它們會(huì)立即返回一個(gè)零值。關(guān)閉上面例子中的naturals變量對(duì)應(yīng)的channel并不能終止循環(huán),它依然會(huì)收到一個(gè)永無(wú)休止的零值序列,然后將它們發(fā)送給打印者goroutine。

沒(méi)有辦法直接測(cè)試一個(gè)channel是否被關(guān)閉,但是接收操作有一個(gè)變體形式:它多接收一個(gè)結(jié)果,多接收的第二個(gè)結(jié)果是一個(gè)布爾值ok,ture表示成功從channels接收到值,false表示channels已經(jīng)被關(guān)閉并且里面沒(méi)有值可接收。使用這個(gè)特性,我們可以修改squarer函數(shù)中的循環(huán)代碼,當(dāng)naturals對(duì)應(yīng)的channel被關(guān)閉并沒(méi)有值可接收時(shí)跳出循環(huán),并且也關(guān)閉squares對(duì)應(yīng)的channel.

// Squarer
go func() {
    for {
        x, ok := <-naturals
        if !ok {
            break // channel was closed and drained
        }
        squares <- x * x
    }
    close(squares)
}()

因?yàn)樯厦娴恼Z(yǔ)法是笨拙的,而且這種處理模式很常見(jiàn),因此Go語(yǔ)言的range循環(huán)可直接在channels上面迭代。使用range循環(huán)是上面處理模式的簡(jiǎn)潔語(yǔ)法,它依次從channel接收數(shù)據(jù),當(dāng)channel被關(guān)閉并且沒(méi)有值可接收時(shí)跳出循環(huán)。

在下面的改進(jìn)中,我們的計(jì)數(shù)器goroutine只生成100個(gè)含數(shù)字的序列,然后關(guān)閉naturals對(duì)應(yīng)的channel,這將導(dǎo)致計(jì)算平方數(shù)的squarer對(duì)應(yīng)的goroutine可以正常終止循環(huán)并關(guān)閉squares對(duì)應(yīng)的channel。(在一個(gè)更復(fù)雜的程序中,可以通過(guò)defer語(yǔ)句關(guān)閉對(duì)應(yīng)的channel。)最后,主goroutine也可以正常終止循環(huán)并退出程序。

gopl.io/ch8/pipeline2

func main() {
    naturals := make(chan int)
    squares := make(chan int)

    // Counter
    go func() {
        for x := 0; x < 100; x++ {
            naturals <- x
        }
        close(naturals)
    }()

    // Squarer
    go func() {
        for x := range naturals {
            squares <- x * x
        }
        close(squares)
    }()

    // Printer (in main goroutine)
    for x := range squares {
        fmt.Println(x)
    }
}

其實(shí)你并不需要關(guān)閉每一個(gè)channel。只有當(dāng)需要告訴接收者goroutine,所有的數(shù)據(jù)已經(jīng)全部發(fā)送時(shí)才需要關(guān)閉channel。不管一個(gè)channel是否被關(guān)閉,當(dāng)它沒(méi)有被引用時(shí)將會(huì)被Go語(yǔ)言的垃圾自動(dòng)回收器回收。(不要將關(guān)閉一個(gè)打開(kāi)文件的操作和關(guān)閉一個(gè)channel操作混淆。對(duì)于每個(gè)打開(kāi)的文件,都需要在不使用的時(shí)候調(diào)用對(duì)應(yīng)的Close方法來(lái)關(guān)閉文件。)

試圖重復(fù)關(guān)閉一個(gè)channel將導(dǎo)致panic異常,試圖關(guān)閉一個(gè)nil值的channel也將導(dǎo)致panic異常。關(guān)閉一個(gè)channels還會(huì)觸發(fā)一個(gè)廣播機(jī)制,我們將在8.9節(jié)討論。

8.4.3. 單方向的Channel

隨著程序的增長(zhǎng),人們習(xí)慣于將大的函數(shù)拆分為小的函數(shù)。我們前面的例子中使用了三個(gè)goroutine,然后用兩個(gè)channels來(lái)連接它們,它們都是main函數(shù)的局部變量。將三個(gè)goroutine拆分為以下三個(gè)函數(shù)是自然的想法:

func counter(out chan int)
func squarer(out, in chan int)
func printer(in chan int)

其中計(jì)算平方的squarer函數(shù)在兩個(gè)串聯(lián)Channels的中間,因此擁有兩個(gè)channel類(lèi)型的參數(shù),一個(gè)用于輸入一個(gè)用于輸出。兩個(gè)channel都擁有相同的類(lèi)型,但是它們的使用方式相反:一個(gè)只用于接收,另一個(gè)只用于發(fā)送。參數(shù)的名字in和out已經(jīng)明確表示了這個(gè)意圖,但是并無(wú)法保證squarer函數(shù)向一個(gè)in參數(shù)對(duì)應(yīng)的channel發(fā)送數(shù)據(jù)或者從一個(gè)out參數(shù)對(duì)應(yīng)的channel接收數(shù)據(jù)。

這種場(chǎng)景是典型的。當(dāng)一個(gè)channel作為一個(gè)函數(shù)參數(shù)時(shí),它一般總是被專(zhuān)門(mén)用于只發(fā)送或者只接收。

為了表明這種意圖并防止被濫用,Go語(yǔ)言的類(lèi)型系統(tǒng)提供了單方向的channel類(lèi)型,分別用于只發(fā)送或只接收的channel。類(lèi)型chan<- int表示一個(gè)只發(fā)送int的channel,只能發(fā)送不能接收。相反,類(lèi)型<-chan int表示一個(gè)只接收int的channel,只能接收不能發(fā)送。(箭頭<-和關(guān)鍵字chan的相對(duì)位置表明了channel的方向。)這種限制將在編譯期檢測(cè)。

因?yàn)殛P(guān)閉操作只用于斷言不再向channel發(fā)送新的數(shù)據(jù),所以只有在發(fā)送者所在的goroutine才會(huì)調(diào)用close函數(shù),因此對(duì)一個(gè)只接收的channel調(diào)用close將是一個(gè)編譯錯(cuò)誤。

這是改進(jìn)的版本,這一次參數(shù)使用了單方向channel類(lèi)型:

gopl.io/ch8/pipeline3

func counter(out chan<- int) {
    for x := 0; x < 100; x++ {
        out <- x
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for v := range in {
        out <- v * v
    }
    close(out)
}

func printer(in <-chan int) {
    for v := range in {
        fmt.Println(v)
    }
}

func main() {
    naturals := make(chan int)
    squares := make(chan int)
    go counter(naturals)
    go squarer(squares, naturals)
    printer(squares)
}

調(diào)用counter(naturals)時(shí),naturals的類(lèi)型將隱式地從chan int轉(zhuǎn)換成chan<- int。調(diào)用printer(squares)也會(huì)導(dǎo)致相似的隱式轉(zhuǎn)換,這一次是轉(zhuǎn)換為<-chan int類(lèi)型只接收型的channel。任何雙向channel向單向channel變量的賦值操作都將導(dǎo)致該隱式轉(zhuǎn)換。這里并沒(méi)有反向轉(zhuǎn)換的語(yǔ)法:也就是不能將一個(gè)類(lèi)似chan<- int類(lèi)型的單向型的channel轉(zhuǎn)換為chan int類(lèi)型的雙向型的channel。

8.4.4. 帶緩存的Channels

帶緩存的Channel內(nèi)部持有一個(gè)元素隊(duì)列。隊(duì)列的最大容量是在調(diào)用make函數(shù)創(chuàng)建channel時(shí)通過(guò)第二個(gè)參數(shù)指定的。下面的語(yǔ)句創(chuàng)建了一個(gè)可以持有三個(gè)字符串元素的帶緩存Channel。圖8.2是ch變量對(duì)應(yīng)的channel的圖形表示形式。

ch = make(chan string, 3)


向緩存Channel的發(fā)送操作就是向內(nèi)部緩存隊(duì)列的尾部插入元素,接收操作則是從隊(duì)列的頭部刪除元素。如果內(nèi)部緩存隊(duì)列是滿(mǎn)的,那么發(fā)送操作將阻塞直到因另一個(gè)goroutine執(zhí)行接收操作而釋放了新的隊(duì)列空間。相反,如果channel是空的,接收操作將阻塞直到有另一個(gè)goroutine執(zhí)行發(fā)送操作而向隊(duì)列插入元素。

我們可以在無(wú)阻塞的情況下連續(xù)向新創(chuàng)建的channel發(fā)送三個(gè)值:

ch <- "A"
ch <- "B"
ch <- "C"

此刻,channel的內(nèi)部緩存隊(duì)列將是滿(mǎn)的(圖8.3),如果有第四個(gè)發(fā)送操作將發(fā)生阻塞。


如果我們接收一個(gè)值,

fmt.Println(<-ch) // "A"

那么channel的緩存隊(duì)列將不是滿(mǎn)的也不是空的(圖8.4),因此對(duì)該channel執(zhí)行的發(fā)送或接收操作都不會(huì)發(fā)生阻塞。通過(guò)這種方式,channel的緩存隊(duì)列解耦了接收和發(fā)送的goroutine。


在某些特殊情況下,程序可能需要知道channel內(nèi)部緩存的容量,可以用內(nèi)置的cap函數(shù)獲取:

fmt.Println(cap(ch)) // "3"

同樣,對(duì)于內(nèi)置的len函數(shù),如果傳入的是channel,那么將返回channel內(nèi)部緩存隊(duì)列中有效元素的個(gè)數(shù)。因?yàn)樵诓l(fā)程序中該信息會(huì)隨著接收操作而失效,但是它對(duì)某些故障診斷和性能優(yōu)化會(huì)有幫助。

fmt.Println(len(ch)) // "2"

在繼續(xù)執(zhí)行兩次接收操作后channel內(nèi)部的緩存隊(duì)列將又成為空的,如果有第四個(gè)接收操作將發(fā)生阻塞:

fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C"

在這個(gè)例子中,發(fā)送和接收操作都發(fā)生在同一個(gè)goroutine中,但是在真實(shí)的程序中它們一般由不同的goroutine執(zhí)行。Go語(yǔ)言新手有時(shí)候會(huì)將一個(gè)帶緩存的channel當(dāng)作同一個(gè)goroutine中的隊(duì)列使用,雖然語(yǔ)法看似簡(jiǎn)單,但實(shí)際上這是一個(gè)錯(cuò)誤。Channel和goroutine的調(diào)度器機(jī)制是緊密相連的,如果沒(méi)有其他goroutine從channel接收,發(fā)送者——或許是整個(gè)程序——將會(huì)面臨永遠(yuǎn)阻塞的風(fēng)險(xiǎn)。如果你只是需要一個(gè)簡(jiǎn)單的隊(duì)列,使用slice就可以了。

下面的例子展示了一個(gè)使用了帶緩存channel的應(yīng)用。它并發(fā)地向三個(gè)鏡像站點(diǎn)發(fā)出請(qǐng)求,三個(gè)鏡像站點(diǎn)分散在不同的地理位置。它們分別將收到的響應(yīng)發(fā)送到帶緩存channel,最后接收者只接收第一個(gè)收到的響應(yīng),也就是最快的那個(gè)響應(yīng)。因此mirroredQuery函數(shù)可能在另外兩個(gè)響應(yīng)慢的鏡像站點(diǎn)響應(yīng)之前就返回了結(jié)果。(順便說(shuō)一下,多個(gè)goroutines并發(fā)地向同一個(gè)channel發(fā)送數(shù)據(jù),或從同一個(gè)channel接收數(shù)據(jù)都是常見(jiàn)的用法。)

func mirroredQuery() string {
    responses := make(chan string, 3)
    go func() { responses <- request("asia.gopl.io") }()
    go func() { responses <- request("europe.gopl.io") }()
    go func() { responses <- request("americas.gopl.io") }()
    return <-responses // return the quickest response
}

func request(hostname string) (response string) { /* ... */ }

如果我們使用了無(wú)緩存的channel,那么兩個(gè)慢的goroutines將會(huì)因?yàn)闆](méi)有人接收而被永遠(yuǎn)卡住。這種情況,稱(chēng)為goroutines泄漏,這將是一個(gè)BUG。和垃圾變量不同,泄漏的goroutines并不會(huì)被自動(dòng)回收,因此確保每個(gè)不再需要的goroutine能正常退出是重要的。

關(guān)于無(wú)緩存或帶緩存channels之間的選擇,或者是帶緩存channels的容量大小的選擇,都可能影響程序的正確性。無(wú)緩存channel更強(qiáng)地保證了每個(gè)發(fā)送操作與相應(yīng)的同步接收操作;但是對(duì)于帶緩存channel,這些操作是解耦的。同樣,即使我們知道將要發(fā)送到一個(gè)channel的信息的數(shù)量上限,創(chuàng)建一個(gè)對(duì)應(yīng)容量大小的帶緩存channel也是不現(xiàn)實(shí)的,因?yàn)檫@要求在執(zhí)行任何接收操作之前緩存所有已經(jīng)發(fā)送的值。如果未能分配足夠的緩存將導(dǎo)致程序死鎖。

Channel的緩存也可能影響程序的性能。想象一家蛋糕店有三個(gè)廚師,一個(gè)烘焙,一個(gè)上糖衣,還有一個(gè)將每個(gè)蛋糕傳遞到它下一個(gè)廚師的生產(chǎn)線(xiàn)。在狹小的廚房空間環(huán)境,每個(gè)廚師在完成蛋糕后必須等待下一個(gè)廚師已經(jīng)準(zhǔn)備好接受它;這類(lèi)似于在一個(gè)無(wú)緩存的channel上進(jìn)行溝通。

如果在每個(gè)廚師之間有一個(gè)放置一個(gè)蛋糕的額外空間,那么每個(gè)廚師就可以將一個(gè)完成的蛋糕臨時(shí)放在那里而馬上進(jìn)入下一個(gè)蛋糕的制作中;這類(lèi)似于將channel的緩存隊(duì)列的容量設(shè)置為1。只要每個(gè)廚師的平均工作效率相近,那么其中大部分的傳輸工作將是迅速的,個(gè)體之間細(xì)小的效率差異將在交接過(guò)程中彌補(bǔ)。如果廚師之間有更大的額外空間——也是就更大容量的緩存隊(duì)列——將可以在不停止生產(chǎn)線(xiàn)的前提下消除更大的效率波動(dòng),例如一個(gè)廚師可以短暫地休息,然后再加快趕上進(jìn)度而不影響其他人。

另一方面,如果生產(chǎn)線(xiàn)的前期階段一直快于后續(xù)階段,那么它們之間的緩存在大部分時(shí)間都將是滿(mǎn)的。相反,如果后續(xù)階段比前期階段更快,那么它們之間的緩存在大部分時(shí)間都將是空的。對(duì)于這類(lèi)場(chǎng)景,額外的緩存并沒(méi)有帶來(lái)任何好處。

生產(chǎn)線(xiàn)的隱喻對(duì)于理解channels和goroutines的工作機(jī)制是很有幫助的。例如,如果第二階段是需要精心制作的復(fù)雜操作,一個(gè)廚師可能無(wú)法跟上第一個(gè)廚師的進(jìn)度,或者是無(wú)法滿(mǎn)足第三階段廚師的需求。要解決這個(gè)問(wèn)題,我們可以再雇傭另一個(gè)廚師來(lái)幫助完成第二階段的工作,他執(zhí)行相同的任務(wù)但是獨(dú)立工作。這類(lèi)似于基于相同的channels創(chuàng)建另一個(gè)獨(dú)立的goroutine。

我們沒(méi)有太多的空間展示全部細(xì)節(jié),但是gopl.io/ch8/cake包模擬了這個(gè)蛋糕店,可以通過(guò)不同的參數(shù)調(diào)整。它還對(duì)上面提到的幾種場(chǎng)景提供對(duì)應(yīng)的基準(zhǔn)測(cè)試(§11.4) 。



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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)