Go語言學習——channel的死鎖其實沒那麼複雜

1 爲何會有信道

協程(goroutine)算是Go的一大新特性,也正是這個大殺器讓Go爲不少路人駐足欣賞,讓信徒們爲之歡呼津津樂道。緩存

協程的使用也很簡單,在Go中使用關鍵字「go「後面跟上要執行的函數即表示新啓動一個協程中執行功能代碼。安全

func main() {
    go test()
    fmt.Println("it is the main goroutine")
    time.Sleep(time.Second * 1)
}

func test() {
    fmt.Println("it is a new goroutine")
}
複製代碼

能夠簡單理解爲,Go中的協程就是一種更輕、支持更高併發的併發機制。bash

仔細看上面的main函數中有一個休眠一秒的操做,若是去掉該行,則打印結果中就沒有「it is a new goroutine」。這是由於新啓的協程還沒來得及運行,主協程就結束了。併發

因此這裏有個問題,咱們怎麼樣才能讓各個協程之間可以知道彼此是否執行完畢呢?異步

顯然,咱們能夠經過上面的方式,讓主協程休眠一秒鐘,等等子協程,確保子協程可以執行完。但做爲一個新型語言不該該使用這麼low的方式啊。連Java這位老前輩都有Future這種異步機制,並且能夠經過get方法來阻塞等待任務的執行,確保能夠第一時間知曉異步進程的執行狀態。函數

因此,Go必需要有過人之處,即另外一個讓路人側目,讓信徒爲之瘋狂的特性——信道(channel)。高併發

2 信道如何使用

信道能夠簡單認爲是協程goroutine之間一個通訊的橋樑,能夠在不一樣的協程裏互通有無穿梭自如,且是線程安全的。ui

2.1 信道分類

信道分爲兩類spa

無緩衝信道線程

ch := make(chan string)
複製代碼

有緩衝信道

ch := make(chan string, 2)
複製代碼

2.2 兩類信道的區別

一、從聲明方式來看,有緩衝帶了容量,即後面的數字,這裏的2表示信道能夠存放兩個stirng類型的變量

二、無緩衝信道自己不存儲信息,它只負責轉手,有人傳給它,它就必需要傳給別人,若是隻有進或者只有出的操做,都會形成阻塞。有緩衝的能夠存儲指定容量個變量,可是超過這個容量再取值也會阻塞。

2.3 兩種信道使用舉例

無緩衝信道

func main() {
    ch := make(chan string)
    go func() {
        ch <- "send"
    }()
    
    fmt.Println(<-ch)
}
複製代碼

在主協程中新啓一個協程且是匿名函數,在子協程中向通道發送「send」,經過打印結果,咱們知道在主線程使用<-ch接收到了傳給ch的值。

<-ch是一種簡寫方式,也可使用str := <-ch方式接收信道值。

上面是在子協程中向信道傳值,並在主協程取值,也能夠反過來,一樣能夠正常打印信道的值。

func main() {
	ch := make(chan string)
	go func() {
		fmt.Println(<-ch)
	}()

	ch <- "send"
}
複製代碼

有緩衝信道

func main() {
    ch := make(chan string, 2)
    ch <- "first"
    ch <- "second"
    
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
複製代碼

執行結果爲

first
second
複製代碼

信道自己結構是一個先進先出的隊列,因此這裏輸出的順序如結果所示。

從代碼來看這裏也不須要從新啓動一個goroutine,也不會發生死鎖(後面會講緣由)。

3 信道的關閉和遍歷

3.1 關閉

信道是能夠關閉的。對於無緩衝和有緩衝信道關閉的語法都是同樣的。

close(channelName)
複製代碼

注意信道關閉了,就不能往信道傳值了,不然會報錯。

func main() {
	ch := make(chan string, 2)
	ch <- "first"
	ch <- "second"

	close(ch)

	ch <- "third"
}
複製代碼

報錯信息

panic: send on closed channel
複製代碼

3.2 遍歷

有緩衝信道是有容量的,因此是能夠遍歷的,而且支持使用咱們熟悉的range遍歷。

func main() {
	chs := make(chan string, 2)
	chs <- "first"
	chs <- "second"

	for ch := range chs {
		fmt.Println(ch)
	}
}
複製代碼

輸出結果爲

first
second
fatal error: all goroutines are asleep - deadlock!
複製代碼

沒錯,若是取完了信道存儲的信息再去取信息,也會死鎖(後面會講)

4 信道死鎖

有了前面的介紹,咱們大概知道了信道是什麼,如何使用信道。

下面就來講說信道死鎖的場景和爲何會死鎖(有些是本身的理解,可能有誤差,若有問題請指正)。

4.1 死鎖現場1

func main() {
    ch := make(chan string)
    
    ch <- "channelValue"
}
複製代碼
func main() {
    ch := make(chan string)
    
    <-ch
}
複製代碼

這兩種狀況,即不管是向無緩衝信道傳值仍是取值,都會發生死鎖。

緣由分析

如上場景是在只有一個goroutine即主goroutine的,且使用的是無緩衝信道的狀況下。

前面提過,無緩衝信道不存儲值,不管是傳值仍是取值都會阻塞。這裏只有一個主協程的狀況下,第一段代碼是阻塞在傳值,第二段代碼是阻塞在取值。由於一直卡住主協程,系統一直在等待,因此係統判斷爲死鎖,最終報deadlock錯誤並結束程序。

延伸

func main() {
    ch := make(chan string)
    go func() {
        ch <- "send"
    }()
}
複製代碼

這種狀況不會發生死鎖。

有人說那是由於主協程發車太快,子協程還沒看到,車就開走了,因此沒來得及抱怨(deadlock)就結束了。

其實不是這樣的,下面舉個反例

func main() {
	ch := make(chan string)
	go func() {
		ch <- "send"
	}()

	time.Sleep(time.Second * 3)
}
複製代碼

此次主協程等你了三秒,三秒你總該完事了吧?!

可是從執行結果來看,並無子協程由於一直阻塞就形成報死鎖錯誤。

這是由於雖然子協程一直阻塞在傳值語句,但這也只是子協程的事。外面的主協程仍是該幹嗎幹嗎,等你三秒以後就發車走人了。由於主協程都結束了,因此子協程也只好結束(畢竟沒搭上車只能回家了,光杵在哪也於事無補)

4.2 死鎖現場2

緊接着上面死鎖現場1的延伸場景,咱們提到延伸場景沒有死鎖是由於主協程發車走了,因此子協程也只能回家。也就是二者沒有耦合的關係。

若是二者經過信道創建了聯繫還會死鎖嗎?

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go func() {
        ch2 <- "ch2 value"
        ch1 <- "ch1 value"
    }()
    
    <- ch1
}
複製代碼

執行結果爲

fatal error: all goroutines are asleep - deadlock!
複製代碼

沒錯,這樣就會發生死鎖。

緣由分析

上面的代碼不能保證是主線程的<-ch1先執行仍是子協程的代碼先執行。

若是主協程先執行到<-ch1,顯然會阻塞等待有其餘協程往ch1傳值。終於等到子協程運行了,結果子協程運行ch2 <- "ch2 value"就阻塞了,由於是無緩衝,因此必須有下家接收值才行,可是等了半天也沒有人來傳值。

因此這時候就出現了主協程等子協程的ch1,子協程在等ch2的接收者,ch1<-「ch1 value」語句遲遲拿不到執行權,因而你們都在相互等待,系統看不下去了,斷定死鎖,程序結束。

相反執行順序也是同樣。

延伸

有人會說那我改爲這樣能避免死鎖嗎

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	go func() {
		ch2 <- "ch2 value"
		ch1 <- "ch1 value"
	}()

	<- ch1
	<- ch2
}
複製代碼

不行,執行結果依然是死鎖。由於這樣的順序仍是改變不了主協程和子協程相互等待的狀況,即死鎖的觸發條件。

改成下面這樣就能夠正常結束

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	go func() {
		ch2 <- "ch2 value"
		ch1 <- "ch1 value"
	}()

	<- ch2
	<- ch1
}
複製代碼

藉此,經過下面的例子再驗證上面死鎖現場1是由於主協程沒受到死鎖的影響因此不會報死鎖錯誤的問題

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	go func() {
		ch2 <- "ch2 value"
		ch1 <- "ch1 value"
	}()

	go func() {
		<- ch1
		<- ch2
	}()

	time.Sleep(time.Second * 2)
}
複製代碼

咱們剛剛看到若是

<- ch1
<- ch2
複製代碼

放到主協程,則會由於相互等待發生死鎖。可是這個例子裏,將一樣的代碼放到一個新啓的協程中,儘管兩個子協程存在阻塞死鎖的狀況,可是不會影響主協程,因此程序執行不會報死鎖錯誤。

4.3 死鎖現場3

func main() {
	chs := make(chan string, 2)
	chs <- "first"
	chs <- "second"

	for ch := range chs {
		fmt.Println(ch)
	}
}
複製代碼

輸出結果爲

first
second
fatal error: all goroutines are asleep - deadlock!
複製代碼

緣由分析

爲何會在輸出完chs信道全部緩存值後會死鎖呢?

其實也很簡單,雖然這裏的chs是帶有緩衝的信道,可是容量只有兩個,當兩個輸出完以後,能夠簡單的將此時的信道等價於無緩衝的信道。

顯然對於無緩衝的信道只是單純的讀取元素是會形成阻塞的,並且是在主協程,因此和死鎖現場1等價,故而會死鎖。

5 總結

一、信道是協程之間溝通的橋樑

二、信道分爲無緩衝信道和有緩衝信道

三、信道使用時要注意是否構成死鎖以及各類死鎖產生的緣由

相關文章
相關標籤/搜索