一看就懂系列之Golang的goroutine和通道

語言: CN / TW / HK

版權宣告:本文為博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/u011957758/article/details/81159481

https://blog.csdn.net/u011957758/article/details/81159481

前言

如果說php是最好的語言,那麼golang就是最併發的語言。

支援golang的併發很重要的一個是goroutine的實現,那麼本文將重點圍繞goroutine來做一下相關的筆記,以便日後快速留戀。

10s後,以下知識點即將靠近:

1.從併發模型說起

2.goroutine的簡介

3.goroutine的使用姿勢

4.通道(channel)的簡介

5.重要的四種通道使用

6.goroutine死鎖與處理

7.select的簡介

8.select的應用場景

9.select死鎖

正文

1.從併發模型說起

看過很多大神簡介,各種研究高併發,那麼就通俗的說下併發。

併發目前來看比較主流的就三種:

1.多執行緒

每個執行緒一次處理一個請求,執行緒越多可併發處理的請求數就越多,但是在高併發下,多執行緒開銷會比較大。

2.協程

無需搶佔式的排程,開銷小,可以有效的提高執行緒的併發性,從而避免了執行緒的缺點的部分

3.基於非同步回撥的IO模型

說一個熟悉的,比如nginx使用的就是epoll模型,通過事件驅動的方式與非同步IO回撥,使得伺服器持續運轉,來支撐高併發的請求

為了追求更高效和低開銷的併發,golang的goroutine來了。

2.goroutine的簡介

定義:在go裡面,每一個併發執行的活動成為goroutine。

詳解:goroutine可以認為是輕量級的 執行緒 ,與建立執行緒相比,建立 成本和開銷都很小 ,每個goroutine的堆疊只有 幾kb ,並且堆疊可根據程式的需要增長和縮小(執行緒的堆疊需指明和固定),所以go程式從語言層面支援了高併發。

程式執行的背後:當一個程式啟動的時候,只有一個goroutine來呼叫main函式,稱它為主goroutine,新的goroutine通過go語句進行建立。

3.goroutine的使用姿勢

3.1單個goroutine建立

在函式或者方法前面加上關鍵字go,即建立一個併發執行的新goroutine。

上程式碼:

package main

import (
    "fmt"
    "time"
)

func HelloWorld() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go HelloWorld()      // 開啟一個新的併發執行
    time.Sleep(1*time.Second)
    fmt.Println("我後面才輸出來")
}

以上執行後會輸出:

Hello world goroutine
我後面才輸出來

需要注意的是,執行速度很快,一定要加sleep,不然你一定可以看到goroutine裡頭的輸出。

這也說明了一個關鍵點: 當main函式返回時,所有的gourutine都是暴力終結的,然後程式退出。

3.2多個goroutine建立

package main

import (
    "fmt"
    "time"
)

func DelayPrint() {
    for i := 1; i <= 4; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Println(i)
    }
}

func HelloWorld() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go DelayPrint()    // 開啟第一個goroutine
    go HelloWorld()    // 開啟第二個goroutine
    time.Sleep(2*time.Second)
    fmt.Println("main function")
}

函式輸出:

Hello world goroutine
1
2
3
4
5
main function

有心的同學可能會發現,DelayPrint裡頭有sleep,那麼會導致第二個goroutine堵塞或者等待嗎?

答案是:no

疑惑: 當程式執行go FUNC()的時候,只是簡單的呼叫然後就立即返回了,並不關心函式裡頭髮生的故事情節,所以不同的goroutine直接不影響,main會繼續按順序執行語句。

4.通道(channel)的簡介

4.1簡介

如果說goroutine是Go併發的執行體,那麼”通道”就是他們之間的連線。

通道可以讓一個goroutine傳送特定的值到另外一個goroutine的通訊機制。

4.2宣告&傳值&關閉

宣告

var ch chan int      // 宣告一個傳遞int型別的channel
ch := make(chan int) // 使用內建函式make()定義一個channel

//=========

ch <- value          // 將一個數據value寫入至channel,這會導致阻塞,直到有其他goroutine從這個channel中讀取資料
value := <-ch        // 從channel中讀取資料,如果channel之前沒有寫入資料,也會導致阻塞,直到channel中被寫入資料為止

//=========

close(ch)            // 關閉channel

有沒注意到關鍵字” 阻塞 “?,這個其實是預設的channel的接收和傳送,其實也有非阻塞的,請看下文。

5.重要的四種通道使用

1.無緩衝通道

說明:無緩衝通道上的傳送操作將會被阻塞,直到另一個goroutine在對應的通道上執行接收操作,此時值才傳送完成,兩個goroutine都繼續執行。

上程式碼:

package main

import (
    "fmt"
    "time"
)
var done chan bool
func HelloWorld() {
    fmt.Println("Hello world goroutine")
    time.Sleep(1*time.Second)
    done <- true
}
func main() {
    done = make(chan bool)  // 建立一個channel
    go HelloWorld()
    <-done
}

輸出:

Hello world goroutine

由於main不會等goroutine執行結束才返回,前文專門加了sleep輸出為了可以看到goroutine的輸出內容,那麼在這裡由於是 阻塞 的,所以無需sleep。

(小嚐試:可以將程式碼中”done <- true”和”<-done”,去掉再執行,看看會發生啥?)

2.管道

通道可以用來連線goroutine,這樣一個的輸出是另一個輸入。這就叫做管道。

例子:

package main

import (
    "fmt"
    "time"
)
var echo chan string
var receive chan string

// 定義goroutine 1 
func Echo() {
    time.Sleep(1*time.Second)
    echo <- "咖啡色的羊駝"
}

// 定義goroutine 2
func Receive() {
    temp := <- echo // 阻塞等待echo的通道的返回
    receive <- temp
}


func main() {
    echo = make(chan string)
    receive = make(chan string)

    go Echo()
    go Receive()

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

在這裡不一定要去關閉channel,因為底層的垃圾回收機制會根據它 是否可以訪問來決定是否自動回收它 。(這裡不是根據channel是否關閉來決定的)

3.單向通道型別

當程式則夠複雜的時候,為了程式碼可讀性更高,拆分成一個一個的小函式是需要的。

此時go提供了單向通道的型別,來實現函式之間channel的傳遞。

上程式碼:

package main

import (
    "fmt"
    "time"
)

// 定義goroutine 1
func Echo(out chan<- string) {   // 定義輸出通道型別
    time.Sleep(1*time.Second)
    out <- "咖啡色的羊駝"
    close(out)
}

// 定義goroutine 2
func Receive(out chan<- string, in <-chan string) { // 定義輸出通道型別和輸入型別
    temp := <-in // 阻塞等待echo的通道的返回
    out <- temp
    close(out)
}


func main() {
    echo := make(chan string)
    receive := make(chan string)

    go Echo(echo)
    go Receive(receive, echo)

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

程式輸出:

咖啡色的羊駝

4.緩衝管道

goroutine的通道預設是是阻塞的,那麼有什麼辦法可以緩解阻塞?

答案是:加一個緩衝區。

對於go來說建立一個緩衝通道很簡單:

ch := make(chan string, 3) // 建立了緩衝區為3的通道

//=========
len(ch)   // 長度計算
cap(ch)   // 容量計算

6.goroutine死鎖與友好退出

6.1goroutine死鎖

來一個死鎖現場一:

package main

func main() {
    ch := make(chan int)
    <- ch // 阻塞main goroutine, 通道被鎖
}

輸出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()

死鎖現場2:

package main

func main() {
    cha, chb := make(chan int), make(chan int)

    go func() {
        cha <- 1 // cha通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine
        chb <- 0
    }()

    <- chb // chb 等待資料的寫
}

為什麼會有死鎖的產生?

非緩衝通道上如果發生了流入無流出,或者流出無流入,就會引起死鎖。

或者這麼說:goroutine的非緩衝通道里頭一定要一進一出,成對出現才行。

上面例子屬於:一:流出無流入;二:流入無流出

當然,有一個例外:

func main() {
    ch := make(chan int)
    go func() {
       ch <- 1
    }()
}

執行以上程式碼將會發現,竟然沒有報錯。

what?

不是說好的一進一出就死鎖嗎?

仔細研究會發現,其實根本沒等goroutine執行完,main函式自己先跑完了,所以就沒有資料流入主的goroutine,就不會被阻塞和報錯

6.2goroutine的死鎖處理

有兩種辦法可以解決:

1.把沒取走的取走便是

package main

func main() {
    cha, chb := make(chan int), make(chan int)

    go func() {
        cha <- 1 // cha通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine
        chb <- 0
    }()

    <- cha // 取走便是
    <- chb // chb 等待資料的寫
}

2.建立緩衝通道

package main

func main() {
    cha, chb := make(chan int, 3), make(chan int)

    go func() {
        cha <- 1 // cha通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine
        chb <- 0
    }()

    <- chb // chb 等待資料的寫
}

這樣的話,cha可以快取一個數據,cha就不會掛起當前的goroutine了。除非再放兩個進去,塞滿緩衝通道就會了。

7.select的簡介

定義:在golang裡頭select的功能與epoll(nginx)/poll/select的功能類似,都是堅挺IO操作,當IO操作發生的時候,觸發相應的動作。

select有幾個重要的點要強調:

1.如果有多個case都可以執行,select會隨機公平地選出一個執行,其他不會執行

上程式碼:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)

    ch<-1
    select {
    case <-ch:
        fmt.Println("咖啡色的羊駝")
    case <-ch:
        fmt.Println("黃色的羊駝")
    }
}

輸出:

(隨機)二者其一

2.case後面必須是channel操作,否則報錯。

上程式碼:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    ch<-1
    select {
    case <-ch:
        fmt.Println("咖啡色的羊駝")
    case 2:
        fmt.Println("黃色的羊駝")
    }
}

輸出報錯:

2 evaluated but not used
select case must be receive, send or assign recv

3.select中的default子句總是可執行的。所以沒有default的select才會阻塞等待事件

上程式碼:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意這裡備註了。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊駝")
    default:
        fmt.Println("黃色的羊駝")
    }
}

輸出:

黃色的羊駝

4.沒有執行的case,那麼江湖阻塞事件發生報錯(死鎖)

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意這裡備註了。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊駝")
    }
}

輸出報錯:

fatal error: all goroutines are asleep - deadlock!

8.select的應用場景

1.timeout 機制(超時判斷)

package main

import (
    "fmt"
    "time"
)

func main() {
    timeout := make (chan bool, 1)
    go func() {
        time.Sleep(1*time.Second) // 休眠1s,如果超過1s還沒I操作則認為超時,通知select已經超時啦~
        timeout <- true
    }()
    ch := make (chan int)
    select {
    case <- ch:
    case <- timeout:
        fmt.Println("超時啦!")
    }
}

以上是入門版,通常程式碼中是這麼寫的:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make (chan int)
    select {
    case <-ch:
    case <-time.After(time.Second * 1): // 利用time來實現,After代表多少時間後執行輸出東西
        fmt.Println("超時啦!")
    }
}

2.判斷channel是否阻塞(或者說channel是否已經滿了)

package main

import (
    "fmt"
)

func main() {
    ch := make (chan int, 1)  // 注意這裡給的容量是1
    ch <- 1
    select {
    case ch <- 2:
    default:
        fmt.Println("通道channel已經滿啦,塞不下東西了!")
    }
}

3.退出機制

package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {
        DONE: 
        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                break DONE // 跳出 select 和 for 迴圈
            default:
            }
        }
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}

輸出:

1532390471
1532390472
1532390473
stop
1532390474

這邊要強調一點:退出迴圈一定要用break + 具體的標記,或者goto也可以。否則其實不是真的退出。

package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {

        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                goto DONE // 跳出 select 和 for 迴圈
            default:
            }
        }
        DONE:
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}

輸出:

1532390525
1532390526
1532390527
1532390528
stop

9.select死鎖

select不注意也會發生死鎖,前文有提到一個,這裡分幾種情況,重點再次強調:

1.如果沒有資料需要傳送,select中又存在接收通道資料的語句,那麼將傳送死鎖

package main
func main() {  
    ch := make(chan string)
    select {
    case <-ch:
    }
}

預防的話加default。

空select,也會引起死鎖

package main

func main() {  
    select {}
}