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

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 {}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章