5. 併發神奇——協程 |《 刻意學習 Golang 》

jerrkill發表於2019-03-12

先簡單梳理一下,下面要講的內容吧,看完 Go 的併發之後 腦子裡就一個東西 『協程』。程式下面有執行緒、執行緒下面有協程。準備找個時間翻譯幾篇官方部落格講 Go 併發內容的,繼續加深理解。

PHP 中沒有協程,Swoole 中實現了協程,所以 Swoole 在併發上面顯得如此優異。

內容簡介

  • 協程
  • 協程之間的通訊 channels 雙向管道
  • 快取管道
  • 使用 range 跟 close 來遍歷快取管道
  • 多個 channels 同時使用時:select
  • 處理超時

goroutine 『協程』

goroutine 是 Go 並行設計的核心。goroutine 說到底其實就是協程,但是它比執行緒更小,十幾個 goroutine 可能體現在底層就是五六個執行緒,Go 語言內部幫你實現了這些 goroutine 之間的記憶體共享。執行 goroutine 只需極少的棧記憶體(大概是 4~5 KB),當然會根據相應的資料伸縮。也正因為如此,可同時執行成千上萬個併發任務。goroutine 比 thread 更易用、更高效、更輕便。

goroutine 是通過 Go 的 runtime 管理的一個執行緒管理器。goroutine 通過 go 關鍵字實現了,其實就是一個普通的函式。

chanels 雙向管道通訊

goroutine 執行再相同的地址空間,訪問共享記憶體必須做好同步,Go 提供了一種機制,用於協程之間進行資料通訊『channels』這個類似於 Unix shell 中的雙向管道。

  • 由 make 建立
  • 建立時需要指定型別
  • 可以接收、傳送指定型別資料
  • 使用 <- 操作符進行接收、傳送資料
  • channels 接收和傳送資料都是阻塞的

下面一一解釋幾點

建立

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

<- 接收傳送資料 (下面分別是兩個協程:一個傳送資料一個接收資料)

// goroutine 1
ch <- v  // 將 v 傳給 ch
// goroutine 2
x <- ch  // 從 ch 接收 v

實際使用(下面通過兩個協程來平行計算 sum 然後將資料通過 channels 傳回)

func sum(a []int, ch chan int) {
    sum := 0
    for _, v := range a {
        sum += v
    }
    ch <- sum  // 將 sum 傳回,可以理解為 push 到 ch 通道
}
int main() {
    a := []int{7, 2, 8, -9, 4, 0}

    ch := make(chan int)
    go sum(a[:len(a)/2], ch) // 開啟一個協程 並用 ch 作為通訊通道
    go sum(a[len(a/2):], ch)

    x, y := <-ch, <-ch // 接收 ch 通道中的值
    fmt.Println(x, y, x + y)
}

所謂阻塞就是需要等待某些事情完成之後才會繼續執行下去否則就處於等待狀態。換句話說就是:

如果 通道 ch 中沒有資料那麼讀取資料的協程就阻塞起來等待直到有資料push入,反之如果push資料時候 ch 中還有資料,也將阻塞起來等待資料被取出再繼續執行。也就是 ch 中只能存在一個資料

我們改一改上面的程式碼你就理解了:

// Sleep 需要 time 包
func sum(a []int, ch chan int) {
    sum := 0
    for _, v := range a {
        sum += v
    }
    fmt.Println("我將 10s 後計算完成再將資料 push 到通道 ch")
    time.Sleep(time.Second * 10)
    ch <- sum
}
int main() {
    a := []int{7, 2, 8, -9, 4, 0}

    ch := make(chan int)
    go sum(a[:len(a)/2], ch) // 開啟一個協程 並用 ch 作為通訊通道
    go sum(a[len(a)/2:], ch)

    fmt.Println("ch 中沒有資料我將阻塞起來等資料")
    x, y := <-ch, <-ch // 接收 ch 通道中的值
    fmt.Println(x, y, x + y)
}

上面程式輸出

ch 沒有資料我阻塞起來等資料
我將 10s 後計算完成將資料push到通道
我將 10s 後計算完成將資料push到通道
// 這裡等待 10s 因為兩個協程併發執行所以只需要等 10s
17 -5 12

我想你應該明白了有關 channels 上面的五點。上面是無快取的(只能存一個資料),下面介紹有快取的 channels(可以存多個資料)

Buffered Channels

Go 也允許指定 channel 的緩衝大小(指定 channels 中同時存在的資料個數)建立時候指定快取長度即可

ch := make(chan type, value) // value 為快取長度
  • value = 0 時,channel 是無緩衝阻塞讀寫的
  • value > 0 時,channel 有緩衝、非阻塞的,直到寫滿 value 個元素才阻塞寫入。

我們看一下下面這個例子,你可以在自己本機測試一下,修改相應的 value 值

Range 與 Close

當通道可以存兩個或者以上的值時候,我們通過 x <- ch y <- ch ... 這就顯得很不明智了,go 提供了 range 跟 close ,讓我們像遍歷陣列一樣來獲取管道中的資料。

func fibonacci(n int, c chan int) {
    x, y := 1, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x + y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    for i := range c {
        fmt.Println(i)
    }
}

記住應該在生產者的地方關閉 channel,而不是消費的地方去關閉它,這樣容易引起 panic

另外一點的就是 channel 不像檔案之類的,不需要經常去關閉,只有當你確實沒有任何傳送資料了,或者你想顯式的結束 range 迴圈之類的

Select

select 的出現是為了解決當我們有多個 channels 時,選擇使用那個 channels,畫張圖更容易理解

超時

出現阻塞了,我們不能讓其無休止的等待下去,所以就有了超時。
請看程式碼

func main () {
    ch := make(chan int)
    ch1 := make(chan int)
    o := make(chan bool)
    go func (){
        for {
            select { // ch/ch1 通道都沒有資料的時候 select 會阻塞起來等待資料,兩個同時有資料時候隨機選一個
            case v := <- ch:
                fmt.Println("v:", v)
            case a := <- ch1:
                fmt.Println("a:", a)
            case <- time.After(2*time.Second):  // 如果前面的所有通道都阻塞了2s就執行這裡
                fmt.Println("time out")
                o <- true
                break
            }
        }
    }()
    // 模擬向通道寫資料
    go func () {
        for i := 0; i < 5; i++ {
            ch <- i + 1
        }
    }()

    go func () {
        for i := 0; i < 5; i++ {
            ch1 <- i + 1
        }
    }()
    // 會阻塞起來等待 o 中的資料
    <- o
}

runtime goroutine

runtime 包中有幾個處理 goroutine 的函式:

  • Goexit

    退出當前執行的 goroutine,但是 defer 函式還會繼續呼叫

  • Gosched

    讓出當前 goroutine 的執行許可權,排程器安排其他等待的任務執行,並在下次某個時候從該位置恢復執行。

  • NumCPU

    返回 CPU 核數量

  • NumGoroutine

    返回正在執行和排隊的任務總數

  • GOMAXPROCS

    用來設定可以平行計算的 CPU 核數的最大值,並返回之前的值。

高度自律,深度思考,以勤補拙

相關文章