golang併發

1.goroutine

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

goroutine是經過Go的runtime管理的一個線程管理器。goroutine經過go關鍵字實現了,其實就是一個普通的函數。shell

go hello(a, b, c)

經過關鍵字go就啓動了一個goroutine。咱們來看一個例子編程

package main

import (
    "fmt"
    "runtime"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world") //開一個新的Goroutines執行
    say("hello") //當前Goroutines執行
}

// 以上程序執行後將輸出:
// hello
// world
// hello
// world
// hello
// world
// hello
// world
// hello

咱們能夠看到go關鍵字很方便的就實現了併發編程。 上面的多個goroutine運行在同一個進程裏面,共享內存數據,不過設計上咱們要遵循:不要經過共享來通訊,而要經過通訊來共享。緩存

1.1 goroutine的調度機制

Go runtime的調度器:
在瞭解Go的運行時的scheduler以前,須要先了解爲何須要它,由於咱們可能會想,OS內核不是已經有一個線程scheduler了嘛?
熟悉POSIX API的人都知道,POSIX的方案在很大程度上是對Unix process進場模型的一個邏輯描述和擴展,二者有不少類似的地方。 Thread有本身的信號掩碼,CPU affinity等。可是不少特徵對於Go程序來講都是累贅。 尤爲是context上下文切換的耗時。另外一個緣由是Go的垃圾回收須要全部的goroutine中止,使得內存在一個一致的狀態。垃圾回收的時間點是不肯定的,若是依靠OS自身的scheduler來調度,那麼會有大量的線程須要中止工做。併發

單獨的開發一個GO得調度器,能夠是其知道在何時內存狀態是一致的,也就是說,當開始垃圾回收時,運行時只須要爲當時正在CPU核上運行的那個線程等待便可,而不是等待全部的線程。函數

用戶空間線程和內核空間線程之間的映射關係有:N:1,1:1和M:N
N:1是說,多個(N)用戶線程始終在一個內核線程上跑,context上下文切換確實很快,可是沒法真正的利用多核。
1:1是說,一個用戶線程就只在一個內核線程上跑,這時能夠利用多核,可是上下文switch很慢。
M:N是說, 多個goroutine在多個內核線程上跑,這個看似能夠集齊上面二者的優點,可是無疑增長了調度的難度。工具

imgimg

Go的調度器內部有三個重要的結構:M,P,S
M:表明真正的內核OS線程,和POSIX裏的thread差很少,真正幹活的人
G:表明一個goroutine,它有本身的棧,instruction pointer和其餘信息(正在等待的channel等等),用於調度。
P:表明調度的上下文,能夠把它看作一個局部的調度器,使go代碼在一個線程上跑,它是實現從N:1到N:M映射的關鍵。測試

imgimg

圖中看,有2個物理線程M,每個M都擁有一個context(P),每個也都有一個正在運行的goroutine。
P的數量能夠經過GOMAXPROCS()來設置,它其實也就表明了真正的併發度,即有多少個goroutine能夠同時運行。
圖中灰色的那些goroutine並無運行,而是出於ready的就緒態,正在等待被調度。P維護着這個隊列(稱之爲runqueue),
Go語言裏,啓動一個goroutine很容易:go function 就行,因此每有一個go語句被執行,runqueue隊列就在其末尾加入一個
goroutine,在下一個調度點,就從runqueue中取出(如何決定取哪一個goroutine?)一個goroutine執行。ui

爲什麼要維護多個上下文P?由於當一個OS線程被阻塞時,P能夠轉而投奔另外一個OS線程!
圖中看到,當一個OS線程M0陷入阻塞時,P轉而在OS線程M1上運行。調度器保證有足夠的線程來運行因此的context P。線程

imgimg

圖中的M1多是被建立,或者從線程緩存中取出。

當MO返回時,它必須嘗試取得一個context P來運行goroutine,通常狀況下,它會從其餘的OS線程那裏steal偷一個context過來,
若是沒有偷到的話,它就把goroutine放在一個global runqueue裏,而後本身就去睡大覺了(放入線程緩存裏)。Contexts們也會週期性的檢查global runqueue,不然global runqueue上的goroutine永遠沒法執行。

imgimg

另外一種狀況是P所分配的任務G很快就執行完了(分配不均),這就致使了一個上下文P閒着沒事兒幹而系統卻任然忙碌。可是若是global runqueue沒有任務G了,那麼P就不得不從其餘的上下文P那裏拿一些G來執行。通常來講,若是上下文P從其餘的上下文P那裏要偷一個任務的話,通常就‘偷’run queue的一半,這就確保了每一個OS線程都能充分的使用。

2.channels

goroutine運行在相同的地址空間,所以訪問共享內存必須作好同步。那麼goroutine之間如何進行數據的通訊呢,Go提供了一個很好的通訊機制channel。channel能夠與Unix shell 中的雙向管道作類比:能夠經過它發送或者接收值。這些值只能是特定的類型:channel類型。定義一個channel時,也須要定義發送到channel的值的類型。注意,必須使用make 建立channel:

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

channel經過操做符<-來接收和發送數據

ch <- v    // 發送v到channel ch.
v := <-ch  // 從ch中接收數據,並賦值給v

咱們把這些應用到咱們的例子中來:

package main

import "fmt"

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

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

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

    fmt.Println(x, y, x + y)
}

默認狀況下,channel接收和發送數據都是阻塞的,除非另外一端已經準備好,這樣就使得Goroutines同步變的更加的簡單,而不須要顯式的lock。所謂阻塞,也就是若是讀取(value := <-ch)它將會被阻塞,直到有數據接收。其次,任何發送(ch<-5)將會被阻塞,直到數據被讀出。無緩衝channel是在多個goroutine之間同步很棒的工具。

2.1Buffered Channels

上面咱們介紹了默認的非緩存類型的channel,不過Go也容許指定channel的緩衝大小,很簡單,就是channel能夠存儲多少元素。ch:= make(chan bool, 4),建立了能夠存儲4個元素的bool 型channel。在這個channel 中,前4個元素能夠無阻塞的寫入。當寫入第5個元素時,代碼將會阻塞,直到其餘goroutine從channel 中讀取一些元素,騰出空間。

ch := make(chan type, value)

/*
value == 0 ! 無緩衝(阻塞)
value > 0 ! 緩衝(非阻塞,直到value 個元素)
*/

咱們看一下下面這個例子,你能夠在本身本機測試一下,修改相應的value值

package main

import "fmt"

func main() {
    c := make(chan int, 2)//修改2爲1就報錯,修改2爲3能夠正常運行
    c <- 1
    c <- 2
    fmt.Println(<-c)
    fmt.Println(<-c)
}
    //修改成1報以下的錯誤:
    //fatal error: all goroutines are asleep - deadlock!

2.2Range和Close

上面這個例子中,咱們須要讀取兩次c,這樣不是很方便,Go考慮到了這一點,因此也能夠經過range,像操做slice或者map同樣操做緩存類型的channel,請看下面的例子

package main

import (
    "fmt"
)

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

for i := range c可以不斷的讀取channel裏面的數據,直到該channel被顯式的關閉。上面代碼咱們看到能夠顯式的關閉channel,生產者經過內置函數close關閉channel。關閉channel以後就沒法再發送任何數據了,在消費方能夠經過語法v, ok := <-ch測試channel是否被關閉。若是ok返回false,那麼說明channel已經沒有任何數據而且已經被關閉。

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

另外記住一點的就是channel不像文件之類的,不須要常常去關閉,只有當你確實沒有任何發送數據了,或者你想顯式的結束range循環之類的

2.3Select

咱們上面介紹的都是隻有一個channel的狀況,那麼若是存在多個channel的時候,咱們該如何操做呢,Go裏面提供了一個關鍵字select,經過select能夠監聽channel上的數據流動。

select默認是阻塞的,只有當監聽的channel中有發送或接收能夠進行時纔會運行,當多個channel都準備好的時候,select是隨機的選擇一個執行的。

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 1, 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)
}

select裏面還有default語法,select其實就是相似switch的功能,default就是當監聽的channel都沒有準備好的時候,默認執行的(select再也不阻塞等待channel)。

select {
case i := <-c:
    // use i
default:
    // 當c阻塞的時候執行這裏
}

2.4超時

有時候會出現goroutine阻塞的狀況,那麼咱們如何避免整個程序進入阻塞的狀況呢?咱們能夠利用select來設置超時,經過以下的方式實現:

func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
                case v := <- c:
                    println(v)
                case <- time.After(5 * time.Second):
                    println("timeout")
                    o <- true
                    break
            }
        }
    }()
    <- o
}

2.5runtime goroutine

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

  • Goexit

    退出當前執行的goroutine,可是defer函數還會繼續調用

  • Gosched

    讓出當前goroutine的執行權限,調度器安排其餘等待的任務運行,並在下次某個時候從該位置恢復執行。

  • NumCPU

    返回 CPU 核數量

  • NumGoroutine

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

  • GOMAXPROCS

    用來設置能夠並行計算的CPU核數的最大值,並返回以前的值。

參考文章:

相關文章
相關標籤/搜索