Golang的goroutine協程和channel通道

一:簡介

由於併發程序要考慮不少的細節,以保證對共享變量的正確訪問,使得併發編程在不少狀況下變得很複雜。
可是Go語言在開發併發時,是比較簡潔的。它經過channel來傳遞數據。數據競爭這個問題在golang的設計上就進行了規避了。它提倡用通訊的方式實現共享,而不要以共享方式來通訊
Go語言用2種手段來實現併發程序,goroutine和channel,其支持順序通訊進程(communicating sequential processes),簡稱爲CSP。CSP是一種現代的併發編程模型,在這種編程模型中,值會在不一樣的運行實例(goroutine)中傳遞。golang

二:Goroutine

在Go語言中,每個併發的執行單元就叫作goroutine。
每一個goroutine都對應一個很是簡單的模型:它是一個併發的執行函數,而且在多個併發的goroutine間,資源是共享的。goroutine很是輕量,建立的開銷不多。編程

goroutine的用法:
直接在函數前加上一個關鍵字:go。
go func() {}bash

例子:數據結構

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("In main")
    go longSleep()
    go shortSleep()

    fmt.Println("sleep ")
    time.Sleep(10 * 1e9)//ns,符號 1e9 表示 1 乘 10 的 9 次方,e=指數
    fmt.Println("the end of main")
}

func longSleep() {
    fmt.Println("longSleep begin")
    time.Sleep(5 * 1e9)
    fmt.Println("longSleep end")
}

func shortSleep() {
    fmt.Println("shortSleep begin")
    time.Sleep(2 * 1e9)
    fmt.Println("shortSleep end")
}

運行結果:併發

In main
sleep
longSleep begin
shortSleep begin
shortSleep end
longSleep end
the end of main

main() ,longSleep() 和 shortSleep() 這3個函數都是獨立的處理單元按順序啓動,而後開始並行運行。爲了模擬
運算時間的損耗,咱們使用了sleep()函數,這個函數能夠按照指定時間來暫停函數或協程執行。app

若是咱們不在main()函數中sleep()較長的時間,那麼main() 函數結束時,其餘協程運行的程序也會結束。main()程序退出,它不會等待任何其餘非main協程的結束。
協程是獨立的處理單元,一旦陸續啓動一些協程,就沒法肯定他們是何時正在開始運行的。函數

三:通道channel

上面咱們講到,協程都是獨立運行的,他們之間沒有通訊。
協程可使用共享變量來通訊,可是不建議這麼作。在Go中有一種特殊的類型channle通道,能夠經過它來進行goroutine之間的通訊,能夠避免共享內存的坑。channel的通訊保證了同步性。
數據經過通道,同一時間只有一個協程能夠訪問數據,因此不會出現數據競爭,設計時就是這樣的。ui

3.1 channel語法

channel也是經過make進行分配的,其返回的是指向底層相關數據結構的引用。設計

  • 一、基礎語法
var chan1 chan string
chan1 = make(chan string)
//or
chan1 := make(chan string)

//int 
intchan := make(chan int)

//函數也能夠
funcchan := chan func()
  • 二、不帶緩衝的channel
var chan2 chan string

chan2 := make(chan string)

chan3 := make(chan string, 0)
  • 三、帶緩衝區的channel
//在make第二個參數加上數字,就變成一個帶緩衝的channel,
//也是一個雙向channel,既能夠讀也能夠寫
chan3 := make(chan string, 4)
  • 四、單向channel
//只發送的channel,在類型後面加上一個箭頭 <-,只能向channel寫數據
var chan4 chan <-int

chan4 := make(chan <-int)
//只接收的channel,箭頭放在chan前面,只能從channel讀取數據
var chan4 <-chan int

chan4 := make(<-chan int) //初始化

3.2 channel特性

基礎特性code

操做 值爲 nil 的 channel 被關閉的 channel 正常的 channel
close panic panic 成功關閉
c<- 永遠阻塞 panic 阻塞或成功發送
<-c 永遠阻塞 永遠不阻塞 阻塞或成功接收

happens-before 特性

  1. 無緩衝時,接收 happens-before 發送
  2. 任何狀況下,發送 happens-before 接收
  3. close happens-before 接收

3.3 channel用法

3.3.一、無緩衝區

  channel無緩衝區,發送方和接收方須要一一配對,否則發送方會一直阻塞,直到數據被接收方取出。
其實無緩衝區channel不論是存消息仍是取消息,都會掛起當前goroutine,除非另一端已經準備好。
無緩衝區的channel永遠不會存數據,只負責數據的流通。

  • 從無緩衝channel取數據,必需要有數據流進來才能夠,不然當前協程阻塞
  • 數據流入無緩衝channel, 若是沒有其餘goroutine來拿走這個數據,那麼當前協程阻塞

注意:
同步的channel不能只在一個協程中發送和接收,由於會被永遠阻塞,數據不能到接收方那裏。

package main

import "fmt"

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

    go func() {
        for d := range chan1 {
            fmt.Println(d)
        }
    }()

    chan1 <- 1 //發送要放在接收協程跑起來後面,由於發送後會阻塞等待接收
    chan1 <- 2
    chan1 <- 3

    close(chan1)
}
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)
}

3.3.二、有緩衝區

 有緩衝區
channel建立一個緩衝區,若是緩衝區已滿,發送方的主進程或者協程會被阻塞,發送方只能在接收方取走數據後才能從阻塞狀態恢復;若是未滿就不會阻塞;若是爲空,接收方的協程會被阻塞。
上面的這種特性,好比能夠控制主進程的退出,由於有時咱們碰到主協程退出了,其餘的子協程尚未運行完成。

package main

import (
    "fmt"
)

//-------------
var ichan = make(chan int, 3)
var str string

func f() {
    str = "hello world"
    ichan <- 0
}

func main() {
    go f()
    <-ichan  //這裏有值,下面纔會運行

    fmt.Println(str)
}
package main

import (
    "fmt"
)

func main() {
    chan1 := make(chan int, 3)
    quit := make(chan bool) //阻塞主進程,防止未處理完的子協程

    go func() {
        for d := range chan1 { //若是data的緩衝區爲空,這個協程會一直阻塞,除非被channel被close
            fmt.Println(d)
        }
        quit <- true
    }()

    chan1 <- 1
    chan1 <- 2
    chan1 <- 3
    chan1 <- 4
    chan1 <- 5
    close(chan1) //用完須要關閉,不然goroutine會被死鎖,由於上面用range,它是不等到信道關閉是不會結束讀取的
    <-quit       //解除阻塞
}

3.3.三、 for...range

上面有的例子是一個一個的取數據,其實golang還提供了for range 來讀取channel中的數據。

package main

import (
    "fmt"
    "time"
)

func main() {

    go func() {
        time.Sleep(1 * time.Hour)
    }()

    c := make(chan int)
    go func() {
        for i := 0; i < 10; i = i + 1 {
            c <- i
        }
        close(c)//若是把close(c)註釋掉,程序會一直阻塞在for …… range那一行
    }()

    for i := range c {
        fmt.Println(i)
    }

    fmt.Println("end!")
}

//range c 產生的迭代值爲channel中發送的值,它會一直迭代直到channel被關閉。
//注意:上面的例子中若是把close(c)註釋掉,程序會一直阻塞在for …… range那一行

3.3.四、select監聽channel

select監測各個channel的數據。
若是有多個channel接收數據,select會隨機選擇一個case來處理。
你還能夠給select加上一個default語句,若是沒有case須要處理,那麼就會選擇default語句。
多個case狀況下,若是沒有default也沒有case須要處理的,那麼select會阻塞,只到某個case須要處理。
注意:nil channel 的操做會一直被阻塞,若是沒有default的話,select會一直被阻塞。

package main

import (
    "fmt"
)

func foo(i int) chan int {
    c := make(chan int)
    go func() {
        c <- i
    }()
    return c
}

func main() {
    c1, c2, c3 := foo(1), foo(2), foo(3)

    ichan := make(chan int)
    //開一個goroutine監聽各個channel數據輸出並收集數據到channel
    go func() {
        for {//for語句循環處理select, 若是隻有一個select,那麼它只會選一個case處理就結束了
            select { //監聽c1,c2,c3流出,並所有流入到ichan
            case v1 := <-c1:
                ichan <- v1
            case v2 := <-c2:
                ichan <- v2
            case v3 := <-c3:
                ichan <- v3
            }
        }
    }()

    //阻塞主協程,取出ichan的數據
    for i := 0; i < 3; i++ {
        fmt.Println(<-ichan) // 從打印來看咱們的數據輸出並非嚴格的1,2,3順序
    }

    fmt.Println("end!")
}

輸出結果:

2
1
3
end!

3.3.五、超時處理

select還有一個應用超時處理的功能。上面說到若是沒有case須要處理,那麼select會一直阻塞,這時候咱們就能夠在一個case下定義一個超時狀況,其餘case沒有數據處理時,到時間點了這個超時case就會處理了,就不會一直阻塞。
咱們用time.After,它返回一個類型爲 <-chan time 的單向channel,在指定時間發送一個當前時間給channel

package main

import (
    "fmt"
    "time"
)

func main() {
    chan1 := make(chan string, 1)
    go func() {
        time.Sleep(time.Second * 3)
        chan1 <- "res1"
    }()

    select {
    case res := <-chan1: //3秒以後纔會有數據進入槽chan1
        fmt.Println(res)
    case <-time.After(time.Second * 1)://定義超時狀況,1秒後超時.這個超時時間比上面的case短,因此先運行這個case
        fmt.Println("timeout 1")
    }
}

輸出:
timeout 1

3.4 close channel

上面的特性咱們列舉了close channel的狀況。

  • channel已經被關閉

close()掉了,你繼續往裏面寫數據,會出現panic。
可是,從這個關閉的channel能夠讀出已發送的數據,還能夠不斷的讀取零值。
若是是經過range讀取數據,channel關閉後for循環會跳出。

經過i, ok := <-c 能夠查看channel的狀態,判斷是零值仍是正常讀取的值。

c := make(chan int, 10)
close(c)
i, ok := <-c
fmt.Printf("%d, %t", i, ok) //0, false

參考

https://colobu.com/2016/04/14/Golang-Channels/

相關文章
相關標籤/搜索