go語言20小時從入門到精通(11、併發編程)

##11.1 概述 ###11.1.1 並行和併發 並行(parallel):指在同一時刻,有多條指令在多個處理器上同時執行。 編程

圖片.png

併發(concurrency):指在同一時刻只能有一條指令執行,但多個進程指令被快速的輪換執行,使得在宏觀上具備多個進程同時執行的效果,但在微觀上並非同時執行的,只是把時間分紅若干段,使多個進程快速交替的執行。 安全

圖片.png

λ 並行是兩個隊列同時使用兩臺咖啡機 λ 併發是兩個隊列交替使用一臺咖啡機 bash

圖片.png

###11.1.2 Go語言併發優點 有人把Go比做21世紀的C語言,第一是由於Go語言設計簡單,第二,21世紀最重要的就是並行程序設計,而Go從語言層面就支持了並行。同時,併發程序的內存管理有時候是很是複雜的,而Go語言提供了自動垃圾回收機制。數據結構

Go語言爲併發編程而內置的上層API基於CSP(communicating sequential processes, 順序通訊進程)模型。這就意味着顯式鎖都是能夠避免的,由於Go語言經過相冊安全的通道發送和接受數據以實現同步,這大大地簡化了併發程序的編寫。併發

通常狀況下,一個普通的桌面計算機跑十幾二十個線程就有點負載過大了,可是一樣這臺機器卻能夠輕鬆地讓成百上千甚至過萬個goroutine進行資源競爭。異步

##11.2 goroutine ###11.2.1 goroutine是什麼 goroutine是Go並行設計的核心。goroutine說到底其實就是協程,可是它比線程更小,十幾個goroutine可能體如今底層就是五六個線程,Go語言內部幫你實現了這些goroutine之間的內存共享。執行goroutine只需極少的棧內存(大概是4~5KB),固然會根據相應的數據伸縮。也正由於如此,可同時運行成千上萬個併發任務。goroutine比thread更易用、更高效、更輕便。 ###11.2.2 建立goroutine 只需在函數調⽤語句前添加 go 關鍵字,就可建立併發執⾏單元。開發⼈員無需瞭解任何執⾏細節,調度器會自動將其安排到合適的系統線程上執行。函數

在併發編程裏,咱們一般想講一個過程切分紅幾塊,而後讓每一個goroutine各自負責一塊工做。當一個程序啓動時,其主函數即在一個單獨的goroutine中運行,咱們叫它main goroutine。新的goroutine會用go語句來建立。ui

示例代碼:spa

package main

import (
    "fmt"
    "time"
)

func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}

func main() {
    //建立一個 goroutine,啓動另一個任務
    go newTask()

    i := 0
    //main goroutine 循環打印
    for {
        i++
        fmt.Printf("main goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}
複製代碼

程序運行結果: 操作系統

圖片.png

###11.2.3 主goroutine先退出 主goroutine退出後,其它的工做goroutine也會自動退出:

func newTask() {
    i := 0
    for {
        i++
        fmt.Printf("new goroutine: i = %d\n", i)
        time.Sleep(1 * time.Second) //延時1s
    }
}

func main() {
    //建立一個 goroutine,啓動另一個任務
    go newTask()

    fmt.Println("main goroutine exit")
}
複製代碼

程序運行結果:

圖片.png

###11.2.4 runtime包 ####11.2.4.1 Gosched runtime.Gosched() 用於讓出CPU時間片,讓出當前goroutine的執行權限,調度器安排其餘等待的任務運行,並在下次某個時候從該位置恢復執行。

這就像跑接力賽,A跑了一會碰到代碼runtime.Gosched() 就把接力棒交給B了,A歇着了,B繼續跑。

示例代碼:

func main() {
    //建立一個goroutine
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")

    for i := 0; i < 2; i++ {
        runtime.Gosched() //import "runtime"
        /*
            屏蔽runtime.Gosched()運行結果以下:
                hello
                hello

            沒有runtime.Gosched()運行結果以下:
                world
                world
                hello
                hello
        */
        fmt.Println("hello")
    }
}
複製代碼

####11.2.4.2 Goexit 調用 runtime.Goexit() 將當即終止當前 goroutine 執⾏,調度器確保全部已註冊 defer延遲調用被執行。

示例代碼:

func main() {
    go func() {
        defer fmt.Println("A.defer")

        func() {
            defer fmt.Println("B.defer")
            runtime.Goexit() // 終止當前 goroutine, import "runtime"
            fmt.Println("B") // 不會執行
        }()

        fmt.Println("A") // 不會執行
    }() //別忘了()

    //死循環,目的不讓主goroutine結束
    for {
    }
}
複製代碼

程序運行結果:

圖片.png

####11.2.4.3 GOMAXPROCS 調用 runtime.GOMAXPROCS() 用來設置能夠並行計算的CPU核數的最大值,並返回以前的值。

示例代碼:

func main() {
    //n := runtime.GOMAXPROCS(1) //打印結果:111111111111111111110000000000000000000011111...
    n := runtime.GOMAXPROCS(2)     //打印結果:010101010101010101011001100101011010010100110...
    fmt.Printf("n = %d\n", n)

    for {
        go fmt.Print(0)
        fmt.Print(1)
    }
}
複製代碼

在第一次執行(runtime.GOMAXPROCS(1))時,最多同時只能有一個goroutine被執行。因此 會打印不少1。過了一段時間後,GO調度器會將其置爲休眠,並喚醒另外一個goroutine,這時候就開始打印不少0了,在打印的時候,goroutine是被調度到操做系統線程上的。

在第二次執行(runtime.GOMAXPROCS(2))時,咱們使用了兩個CPU,因此兩個goroutine能夠一塊兒被執行,以一樣的頻率交替打印0和1。

##11.3 channel goroutine運行在相同的地址空間,所以訪問共享內存必須作好同步。goroutine 奉行經過通訊來共享內存,而不是共享內存來通訊。

引⽤類型 channel 是 CSP 模式的具體實現,用於多個 goroutine 通信。其內部實現了同步,確保併發安全。 ###11.3.1 channel類型 和map相似,channel也一個對應make建立的底層數據結構的引用。

當咱們複製一個channel或用於函數參數傳遞時,咱們只是拷貝了一個channel引用,所以調用者何被調用者將引用同一個channel對象。和其它的引用類型同樣,channel的零值也是nil。

定義一個channel時,也須要定義發送到channel的值的類型。channel可使用內置的make()函數來建立:

    make(chan Type) //等價於make(chan Type, 0)
    make(chan Type, capacity)
複製代碼

當 capacity= 0 時,channel 是無緩衝阻塞讀寫的,當capacity> 0 時,channel 有緩衝、是非阻塞的,直到寫滿 capacity個元素才阻塞寫入。

channel經過操做符<-來接收和發送數據,發送和接收數據語法:     channel <- value      //發送value到channel     <-channel             //接收並將其丟棄     x := <-channel        //從channel中接收數據,並賦值給x     x, ok := <-channel    //功能同上,同時檢查通道是否已關閉或者是否爲空

默認狀況下,channel接收和發送數據都是阻塞的,除非另外一端已經準備好,這樣就使得goroutine同步變的更加的簡單,而不須要顯式的lock。

示例代碼:

func main() {
    c := make(chan int)

    go func() {
        defer fmt.Println("子協程結束")

        fmt.Println("子協程正在運行……")

        c <- 666 //666發送到c
    }()

    num := <-c //從c中接收數據,並賦值給num

    fmt.Println("num = ", num)
    fmt.Println("main協程結束")
}
複製代碼

程序運行結果:

圖片.png

###11.3.2 無緩衝的channel 無緩衝的通道(unbuffered channel)是指在接收前沒有能力保存任何值的通道。

這種類型的通道要求發送 goroutine 和接收 goroutine 同時準備好,才能完成發送和接收操做。若是兩個goroutine沒有同時準備好,通道會致使先執行發送或接收操做的 goroutine 阻塞等待。

這種對通道進行發送和接收的交互行爲自己就是同步的。其中任意一個操做都沒法離開另外一個操做單獨存在。

下圖展現兩個 goroutine 如何利用無緩衝的通道來共享一個值:

圖片.png

在第 1 步,兩個 goroutine 都到達通道,但哪一個都沒有開始執行發送或者接收。 在第 2 步,左側的 goroutine 將它的手伸進了通道,這模擬了向通道發送數據的行爲。這時,這個 goroutine 會在通道中被鎖住,直到交換完成。 在第 3 步,右側的 goroutine 將它的手放入通道,這模擬了從通道里接收數據。這個 goroutine 同樣也會在通道中被鎖住,直到交換完成。 在第 4 步和第 5 步,進行交換,並最終,在第 6 步,兩個 goroutine 都將它們的手從通道里拿出來,這模擬了被鎖住的 goroutine 獲得釋放。兩個 goroutine 如今均可以去作別的事情了。

無緩衝的channel建立格式:     make(chan Type) //等價於make(chan Type, 0)

若是沒有指定緩衝區容量,那麼該通道就是同步的,所以會阻塞到發送者準備好發送和接收者準備好接收。

示例代碼:

func main() {
    c := make(chan int, 0) //無緩衝的通道

    //內置函數 len 返回未被讀取的緩衝元素數量, cap 返回緩衝區大小
    fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))

    go func() {
        defer fmt.Println("子協程結束")

        for i := 0; i < 3; i++ {
            c <- i
            fmt.Printf("子協程正在運行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
        }
    }()

    time.Sleep(2 * time.Second) //延時2s

    for i := 0; i < 3; i++ {
        num := <-c //從c中接收數據,並賦值給num
        fmt.Println("num = ", num)
    }

    fmt.Println("main協程結束")
}
複製代碼

程序運行結果:

圖片.png

###11.3.3 有緩衝的channel 有緩衝的通道(buffered channel)是一種在被接收前能存儲一個或者多個值的通道。

這種類型的通道並不強制要求 goroutine 之間必須同時完成發送和接收。通道會阻塞發送和接收動做的條件也會不一樣。只有在通道中沒有要接收的值時,接收動做纔會阻塞。只有在通道沒有可用緩衝區容納被髮送的值時,發送動做纔會阻塞。

這致使有緩衝的通道和無緩衝的通道之間的一個很大的不一樣:無緩衝的通道保證進行發送和接收的 goroutine 會在同一時間進行數據交換;有緩衝的通道沒有這種保證。 示例圖以下:

圖片.png

在第 1 步,右側的 goroutine 正在從通道接收一個值。 在第 2 步,右側的這個 goroutine獨立完成了接收值的動做,而左側的 goroutine 正在發送一個新值到通道里。 在第 3 步,左側的goroutine 還在向通道發送新值,而右側的 goroutine 正在從通道接收另一個值。這個步驟裏的兩個操做既不是同步的,也不會互相阻塞。 最後,在第 4 步,全部的發送和接收都完成,而通道里還有幾個值,也有一些空間能夠存更多的值。

有緩衝的channel建立格式:     make(chan Type, capacity)

若是給定了一個緩衝區容量,通道就是異步的。只要緩衝區有未使用空間用於發送數據,或還包含能夠接收的數據,那麼其通訊就會無阻塞地進行。

示例代碼:

func main() {
    c := make(chan int, 3) //帶緩衝的通道

    //內置函數 len 返回未被讀取的緩衝元素數量, cap 返回緩衝區大小
    fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))

    go func() {
        defer fmt.Println("子協程結束")

        for i := 0; i < 3; i++ {
            c <- i
            fmt.Printf("子協程正在運行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
        }
    }()

    time.Sleep(2 * time.Second) //延時2s
    for i := 0; i < 3; i++ {
        num := <-c //從c中接收數據,並賦值給num
        fmt.Println("num = ", num)
    }
    fmt.Println("main協程結束")
}
複製代碼

程序運行結果:

圖片.png

###11.3.4 range和close 若是發送者知道,沒有更多的值須要發送到channel的話,那麼讓接收者也能及時知道沒有多餘的值可接收將是有用的,由於接收者能夠中止沒必要要的接收等待。這能夠經過內置的close函數來關閉channel實現。

示例代碼:

func main() {
    c := make(chan int)

    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        //把 close(c) 註釋掉,程序會一直阻塞在 if data, ok := <-c; ok 那一行
        close(c)
    }()

    for {
        //ok爲true說明channel沒有關閉,爲false說明管道已經關閉
        if data, ok := <-c; ok {
            fmt.Println(data)
        } else {
            break
        }
    }

    fmt.Println("Finished")
}
複製代碼

程序運行結果:

圖片.png

注意點: channel不像文件同樣須要常常去關閉,只有當你確實沒有任何發送數據了,或者你想顯式的結束range循環之類的,纔去關閉channel; 關閉channel後,沒法向channel 再發送數據(引起 panic 錯誤後致使接收當即返回零值); 關閉channel後,能夠繼續向channel接收數據; 對於nil channel,不管收發都會被阻塞。

可使用 range 來迭代不斷操做channel:

func main() {
    c := make(chan int)

    go func() {
        for i := 0; i < 5; i++ {
            c <- i
        }
        //把 close(c) 註釋掉,程序會一直阻塞在 for data := range c 那一行
        close(c)
    }()

    for data := range c {
        fmt.Println(data)
    }
    fmt.Println("Finished")
}
複製代碼

###11.3.5 單方向的channel 默認狀況下,通道是雙向的,也就是,既能夠往裏面發送數據也能夠同裏面接收數據。

可是,咱們常常見一個通道做爲參數進行傳遞而值但願對方是單向使用的,要麼只讓它發送數據,要麼只讓它接收數據,這時候咱們能夠指定通道的方向。

單向channel變量的聲明很是簡單,以下: var ch1 chan int // ch1是一個正常的channel,不是單向的 var ch2 chan<- float64 // ch2是單向channel,只用於寫float64數據 var ch3 <-chan int // ch3是單向channel,只用於讀取int數據

chan<- 表示數據進入管道,要把數據寫進管道,對於調用者就是輸出。 <-chan 表示數據從管道出來,對於調用者就是獲得管道的數據,固然就是輸入。

能夠將 channel 隱式轉換爲單向隊列,只收或只發,不能將單向 channel 轉換爲普通 channel:

    c := make(chan int, 3)
    var send chan<- int = c // send-only
    var recv <-chan int = c // receive-only
    send <- 1
    //<-send //invalid operation: <-send (receive from send-only type chan<- int)
    <-recv
    //recv <- 2 //invalid operation: recv <- 2 (send to receive-only type <-chan int)

    //不能將單向 channel 轉換爲普通 channel
    d1 := (chan int)(send) //cannot convert send (type chan<- int) to type chan int
    d2 := (chan int)(recv) //cannot convert recv (type <-chan int) to type chan int

示例代碼:
//   chan<- //只寫
func counter(out chan<- int) {
    defer close(out)
    for i := 0; i < 5; i++ {
        out <- i //若是對方不讀 會阻塞
    }
}

//   <-chan //只讀
func printer(in <-chan int) {
    for num := range in {
        fmt.Println(num)
    }
}

func main() {
    c := make(chan int) //   chan   //讀寫

    go counter(c) //生產者
    printer(c)    //消費者

    fmt.Println("done")
}
複製代碼

###11.3.6 定時器 ####11.3.6.1 Timer Timer是一個定時器,表明將來的一個單一事件,你能夠告訴timer你要等待多長時間,它提供一個channel,在未來的那個時間那個channel提供了一個時間值。

示例代碼:

import "fmt"
import "time"

func main() {
    //建立定時器,2秒後,定時器就會向本身的C字節發送一個time.Time類型的元素值
    timer1 := time.NewTimer(time.Second * 2)
    t1 := time.Now() //當前時間
    fmt.Printf("t1: %v\n", t1)

    t2 := <-timer1.C
    fmt.Printf("t2: %v\n", t2)

    //若是隻是想單純的等待的話,可使用 time.Sleep 來實現
    timer2 := time.NewTimer(time.Second * 2)
    <-timer2.C
    fmt.Println("2s後")

    time.Sleep(time.Second * 2)
    fmt.Println("再一次2s後")

    <-time.After(time.Second * 2)
    fmt.Println("再再一次2s後")

    timer3 := time.NewTimer(time.Second)
    go func() {
        <-timer3.C
        fmt.Println("Timer 3 expired")
    }()

    stop := timer3.Stop() //中止定時器
    if stop {
        fmt.Println("Timer 3 stopped")
    }

    fmt.Println("before")
    timer4 := time.NewTimer(time.Second * 5) //原來設置3s
    timer4.Reset(time.Second * 1)            //從新設置時間
    <-timer4.C
    fmt.Println("after")
}
複製代碼

####11.3.6.2 Ticker Ticker是一個定時觸發的計時器,它會以一個間隔(interval)往channel發送一個事件(當前時間),而channel的接收者能夠以固定的時間間隔從channel中讀取事件。

示例代碼:

func main() {
    //建立定時器,每隔1秒後,定時器就會給channel發送一個事件(當前時間)
    ticker := time.NewTicker(time.Second * 1)

    i := 0
    go func() {
        for { //循環
            <-ticker.C
            i++
            fmt.Println("i = ", i)

            if i == 5 {
                ticker.Stop() //中止定時器
            }
        }
    }() //別忘了()

    //死循環,特意不讓main goroutine結束
    for {
    }
}
複製代碼

##11.4 select ###11.4.1 select做用 Go裏面提供了一個關鍵字select,經過select能夠監聽channel上的數據流動。

select的用法與switch語言很是相似,由select開始一個新的選擇塊,每一個選擇條件由case語句來描述。

與switch語句能夠選擇任何可以使用相等比較的條件相比, select有比較多的限制,其中最大的一條限制就是每一個case語句裏必須是一個IO操做,大體的結構以下:

    select {
    case <-chan1:
        // 若是chan1成功讀到數據,則進行該case處理語句
    case chan2 <- 1:
        // 若是成功向chan2寫入數據,則進行該case處理語句
    default:
        // 若是上面都沒有成功,則進入default處理流程
    }
複製代碼

在一個select語句中,Go語言會按順序從頭到尾評估每個發送和接收的語句。

若是其中的任意一語句能夠繼續執行(即沒有被阻塞),那麼就從那些能夠執行的語句中任意選擇一條來使用。

若是沒有任意一條語句能夠執行(即全部的通道都被阻塞),那麼有兩種可能的狀況: 若是給出了default語句,那麼就會執行default語句,同時程序的執行會從select語句後的語句中恢復。 若是沒有default語句,那麼select語句將被阻塞,直到至少有一個通訊能夠進行下去。

示例代碼:

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 < 6; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()

    fibonacci(c, quit)
}
複製代碼

運行結果以下:

圖片.png

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

func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
            case v := <-c:
                fmt.Println(v)
            case <-time.After(5 * time.Second):
                fmt.Println("timeout")
                o <- true
                break
            }
        }
    }()
    //c <- 666 // 註釋掉,引起 timeout
    <-o
}
複製代碼
相關文章
相關標籤/搜索