Go語言的併發機制

寫在前面

併發 Concurrency

  • go的併發是經過goroutine來實現的
  • 它的使用,就是在某個操做前加上go關鍵字
go f(x, y, z)
  • 什麼叫當前的goroutine?
    • 文字蒼白,且看代碼:
    package main
    import "fmt"
    
    func main() {
        fmt.println("hello,world!")
    }
    • 若是程序開始執行,那麼上面的的就是當前的goroutine了
  • 什麼是非當前的goroutine
    • 使用go關鍵詞就能添加新的goroutine了
    import "fmt"
    
    func NewGoroutine() {
        fmt.Println("new goroutine!")
    }
    
    func main() {
        go NewGoroutine()       //這裏就添加了新的goroutine了(運行起來纔算)
        fmt.println("hello,world!")
    }
    • 可是,運行起來並無看到打印出"new goroutine"
    • 這是由於當前的goroutine並無義務等待這個新的goroutine執行,它本身執行完就結束了
    • 那麼應該如何改變呢?Go提供了一些列管理goroutine的方法,其中最主要就是channel和Mutex了

Channel

  • Channel也是一種類型,它的定義和使用比較特別
ch := make(chan int)
ch <- v     // Send v to channel ch.
v := <-ch  // Receive from ch, and
                // assign value to v.
  • 定義通常採用make函數進行,chan關鍵字是固定的,後面該channel能夠放置的類型,例子中的就是int
  • 它的基本使用有兩種模式:
    • 其餘變量往channel傳送數據:ch<-v
    • 其餘變量從channel接受數據:v := <-ch
  • 兩種操做是互相等待的,以例子中的說明
  • 當A處的數據創送給ch時(ch <- A),它會先判斷,是否其它地方須要從ch中獲取數據
    • 如果:則數據從A處傳到指定的地方
    • 若不是:A處的goroutine將被阻塞(其實也就是停下來),等到其它任意地方須要從ch中獲取數據時,A處的goroutine纔開始運行
  • 而B處須要從ch獲取數據(B := <-ch),它會先判斷,是否某個地方給ch傳送數據
    • 如果:則數據從某個地方傳送到B處
    • 若不是:B處的goroutine將被阻塞(其實也就是停下來),等到其它任意地方傳送數據給ch,B處的goroutine纔開始運行
  • 簡單的比喻就是傳送門兩側,須要等待兩邊的人都贊成才能打開
  • 或者這樣理解,若是把這個過程比喻成接力賽
    • 接力棒是數據,每一個隊有多個成員
    • A成員的目標就是將接力棒交給B成員,但須要奔跑,須要時間,B成員須要等待A成員(被阻塞)
  • 下面是例子代碼:
package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // send sum to c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // receive from c

    fmt.Println(x, y, x+y)
}
  • 注意到 x, y := <-c, <-c // receive from c :
    • 這段代碼在當前goroutine中出現,當程序運行起來,這個位置的channel c將會等待從某處傳來的數據
    • 而某處就是go關鍵字標記出來的兩個新goroutine了:
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    • 這兩處都是執行sum函數,執行的最後把結果傳給channel c
    • 這就解決了當前goroutine不等待其它goroutine運行結束就自個兒運行完成的問題
  • buffered channel是容許存儲多個值的channel,它的空間能夠理解爲一個數組
  • 至關於接力賽中,A隊員要拿着多根接力棒跑,他很辛苦,不僅要給B隊員,還要給C隊員,還要。。。。
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
  • 讀者們能夠試着把ch定義成普通的channel:ch := make(chan int)
  • 這樣會報錯,緣由在於:
    • 首先這是在一個goroutine中
    • 其次,ch <-1執行時,會一直等待其它goroutine執行從ch中拿數據的操做

關鍵字:range,close

  • 當某個goroutine中的某個操做須要不停從channel c中獲取數據時,能夠用到range關鍵字
  • 而對於上述操做,並不須要擔憂當channel c再也不有數據的事情,這個事情是由運行發送數據到channel c的goroutine關心的
  • 若是發送數據給channel c的goroutine不須要再發送數據了,那就須要執行close(c)的操做,不然程序會報錯
package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 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)
    }
}
  • 對於上述代碼,先關注main函數
    • go fibonacci(cap(c), c)這一句新建了一個goroutine,其中cap(c)是用來計算channel c的容量
    • for i := range c這句,
      • 將不停循環判斷,
      • 一有數據傳送到channel c,i 就能獲取該數據,並執行循環語句中的內容
      • 當channel c被關閉,循環判斷將結束
  • 再看fibonacci函數
    • fibonacci數列的計算,其中for循環裏的內容c <- x這句,就是向channel c傳送數據,以後main中的for語句會執行相應操做
    • 最後close(c)這句,關閉channel c,以後main的for語句結束執行

關鍵字 select

  • select很特別,它通常會搭配for循環使用,行爲上和switch語句相似
  • select語句判斷是是否執行了某些操做,若是是,我就將執行XXX操做
  • switch語句判斷的一般是某某值是否等於某某值這種true or false的問題
  • 代碼:
package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}
  • main()中,建立了channel c和channel quit
  • 一個匿名函數做爲新建goroutine的運行內容
  • 而後運行fibonacci函數(這是當前goroutine中的)
    • 在fibonacci函數中,使用for進行無限循環不停執行select語句
    • 在這裏select語句會不停判斷每一個case語句中的操做
    • 像case c <- x,判斷條件也是一種操做,因此會先把x傳送給c,而後阻塞,等待其餘向channel c要數據的操做
      • 這時,匿名函數(新建的goroutine)的for循環中fmt.Println(<-c)將會執行,執行完成後,又會進行等待其餘操做向channel c 傳遞數據
      • 而在fibonacci中,case c <- x能順利進行後,將執行x, y = y, x+y,而後進入新的循環中執行select語句
  • 過程就像上述那樣,進行循環,判斷,阻塞,解除阻塞。。。
  • 等到goroutine的循環結束,並執行quit <- 0後,就會執行select語句中的case <-quit的內容了,以後程序返回,當前goroutine結束
  • 固然,select也會搭配關鍵字default:
package main

import (
    "fmt"
    "time"
)

func main() {
    tick := time.Tick(100 * time.Millisecond)
    boom := time.After(500 * time.Millisecond)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(50 * time.Millisecond)
        }
    }
}
  • 當其它case中的channel都每準備好,就會執行default中的語句

sync.Mutex

  • 解決併發衝突的辦法還有比較經典的Mutex
  • Mutex能夠進行加鎖Lock()和解鎖操做Unlock()
  • 這裏不展開了,留在下一篇,也是a tour of go的最後一道習題,我的認爲這個題目出得很是好,值得詳細講解
相關文章
相關標籤/搜索