go語言聖經第八章(讀書筆記)

第八章 Goroutines和Channels

  • 備註:
    1.這一部分開始,原書gopl開始出現較多的中文錯誤
    2.這一章節有大量例子,此處省去

Goroutines

  • 在Go語言中,每個併發的執行單元叫作一個goroutine
  • 當一個程序啓動,其主函數將在一個單獨的goroutine中運行,叫作main goroutine
  • 可使用go關鍵字來建立新的goroutine
f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
  • 除了從主函數退出或者直接終止程序以外,沒有其它的編程方法可以讓一個goroutine來打斷另外一個的執行

Channels

  • 若是說goroutine是Go語言程序的併發體的話,那麼channels就是它們之間的通訊機制
  • 每個channel都有一個特殊的類型,也就是channel可發送數據的類型
ch := make(chan int) // ch has type 'chan int'
  • 和map相似,chan也是使用make建立的底層數據結構的引用
  • 當咱們複製一個chan或用於函數傳參時,只是拷貝了一個chan的引用
  • chan類型的零值是nil
  • 兩個相同類型的chan可使用==運算符進行比較:
    1.若是兩個chan引用的是相同的對象,那麼比較的結果爲真
    2.一個chan也能夠和nil進行比較
  • 一個channel有發送和接受兩種操做,都是通訊行爲:
    1.一個發送語句將一個值從一個goroutine經過channel發送到另外一個執行接收操做的goroutine
    2.發送和接收都用<-運算符
    3.發送語句中,channel在左,要發送的值在右
    4.接受語句中,channel在右,若是使用變量接收,須要用到=
ch <- x // a send statement
x = <-ch // a receive expression in an assignment statement
<-ch // a receive statement; result is discarded
  • channel還有close操做,使用close操做後:
    1.基於該channel的任何發送操做都將致使panic
    2.1.基於該channel的任何接收操做依然能夠接收以前已經成功發送的數據
    2.2.若是channel中已經沒有數據,將產生一個零值的數據
close(ch)
  • 使用make建立chan,能夠指定參數來實現一個有緩衝的channel(默認是無緩衝的)
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3

不帶緩存的Channel

  • 基於無緩存的Chan之間:
    1.若是發送者goroutine的發送操做先執行,那麼發送者goroutine將被阻塞,直到另外一個接收者goroutine在相同的Chan上執行接收操做。
    2.若是接收者goroutine的接收操做先發生,那麼接收者goroutine將被阻塞,直到有另外一個發送者goroutine在相同的Chan上執行發送操做
    3.當發送的值被成功接收後,兩個goroutine才能夠繼續執行後續語句
  • 基於無緩存的Chan的發送和接收操做將致使兩個goroutine作一次同步操做,所以這個無緩存的Chan也被稱爲同步Chan
  • 當經過一個無緩存的Chan發送數據時,接收者會先被喚醒,而後接收數據(詳細須要深究Go語言的併發內存模型,happens before概念)
  • 一個基本問題:主的goroutine的完成一般不會等待其它goroutine完成
  • 可使用一個chan來解決這個同步問題
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
}

串聯的Channels(Pipeline)

  • Channels頁能夠用於將多個goroutine連接在一塊兒
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)
    }
}
  • "像這樣的串聯Channels的管道(Pipelines)能夠用在須要長時間運行的服務中,每一個長時間運行的goroutine可能會包含一個死循環,在不一樣goroutine的死循環內部使用串聯的Channels來通訊"
  • 如何發送有限的數列?能夠基於如下條件:
    1.當一個chan被關閉,再向chan發送數據會引發panic
    2.當一個chan被關閉,已經發送的數據被成功接收後,後續的接收操做會收到一個零值
    3.沒有辦法直接測試一個chan是否被關閉,可是接收操做有一個變體形式:
// Squarer
go func() {
    for {
        x, ok := <-naturals
        if !ok {
            break // channel was closed and drained
        }
        squares <- x * x
    }
    close(squares)
}()
  • 經過ok變量能夠知道是否從chan中成功接收到值
  • 可使用range循環是上面處理模式的簡潔語法,它依次從channel接收數據,當channel被關閉而且沒有值可接收時跳出循環
  • 改進版本:
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)
    }
}
  • 試圖關閉一個chan兩次或以上,將致使panic
  • 試圖關閉一個nil值的chan也會引發panic
  • 關閉一個channel還會觸發一個廣播機制

併發的退出(調整了原書的順序,原書gopl-zh 332頁)

  • 有時候咱們須要通知goroutine中止它正在乾的事情
  • Go沒有提供在一個goroutine中終止另外一個goroutine的方法
  • 使用內置的close函數會觸發一個廣播

單方向的Channel

  • chan T表示一個既能接收又能發送T類型數據的chan
  • chan<- T 類型表示一個只發送T類型數據的chan
  • <-chan T類型表示一個只接收T類型數據的chan
  • 以上的限制會在編譯期檢測
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)
}

帶緩存的Channels

  • 帶緩存的Chan內部是一個元素隊列,隊列最大容量在使用make建立chan時指定
ch = make(chan string, 3)

  • 對於上面的例子,咱們能夠在無阻塞的狀況下,連續向chan發送三個值
ch <- "A"
ch <- "B"
ch <- "C"

  • 若是咱們接收一個值
fmt.Println(<-ch) //"A"

  • 這時,chan裏的緩存隊列不是空的,也不是滿的,能夠在無阻塞的狀況下,對其進行發送和接收操做
  • 可使用len函數獲取chan的有效元素個數
  • 可使用cap函數獲取chan的緩存隊列容量
  • 若是再發生兩次接收操做,隊列將變空
fmt.Println(<-ch) // "B"
fmt.Println(<-ch) // "C"
  • 以後的接收操做的goroutine將被阻塞
  • 不可將帶緩存的chan看成同一個goroutine中的隊列使用。channel和goroutine的調度器機制是緊密向量的,一個發送操做或許是整個程序,這樣會形成永久的阻塞
  • 下面有一個bug程序,根據原書例子(gopl-zh 310頁)改編
func mirroredQuery() string {
    responses := make(chan string)
    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) { /* ... */ }
  • 以上程序用的是無緩存的chan,隨着函數被執行,一定會有兩個goroutine被一直阻塞,這就形成了goroutines泄漏。和垃圾變量不一樣,泄漏的goroutines並不會被自動回收
  • 所以,確保每一個再也不須要的goroutine能正常退出是重要的

無緩存和有緩存chan的選擇

  • 無緩存:更強調操做之間的同步接收操做
  • 有緩存:接收操做解耦,像cpu,cache,memory之間關係,原書中用了蛋糕店做爲例子(gopl-zh 310頁)

併發的循環

  • "子問題都是徹底彼此獨立的問題被叫作易並行問題(譯註:embarrassingly parallel,直譯的話更像是尷尬並行)"
  • 回顧匿名函數調用循環變量問題
var rmdirs []func()
for _, dir := range tempDirs() {
    os.MkdirAll(dir, 0755)
    rmdirs = append(rmdirs, func() {
        os.RemoveAll(dir) // NOTE: incorrect!
    })
}
  • 以上例子,匿名函數中的函數體使用os.RemoveAll(dir),其實質上是使用了循環變量dir的地址,而不是循環變量某一時候的值,而os.MkdirAll(dir, 0755)由於是函數傳參,得到dir的一份拷貝因此沒問題
  • gopl-zh 312頁例子頗有價值,直接去看便可

基於select的多路複用

select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
  • 上面是select語句的通常形式,每個分支表明一個通訊操做
  • "select會等待case中有可以執行的case時去執行。當條件知足時,select纔會去通訊並執行case以後的語句;這時候其它通訊是不會執行的。一個沒有任何case的select語句寫做select{},會永遠地等待下去"
相關文章
相關標籤/搜索