1. 程式人生 > >Go語言學習——channel的死鎖其實沒那麼複雜

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中的協程就是一種更輕、支援更高併發的併發機制。

  仔細看上面的main函式中有一個休眠一秒的操作,如果去掉該行,則列印結果中就沒有“it is a new goroutine”。這是因為新啟的協程還沒來得及執行,主協程就結束了。

 

  所以這裡有個問題,我們怎麼樣才能讓各個協程之間能夠知道彼此是否執行完畢呢?

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

  所以,Go必須要有過人之處,即另一個讓路人側目,讓信徒為之瘋狂的特性——通道(channel)。

 

2 通道如何使用

  通道可以簡單認為是協程goroutine之間一個通訊的橋樑,可以在不同的協程裡互通有無穿梭自如,且是執行緒安全的。

2.1 通道分類

  通道分為兩類

無緩衝通道

ch := make(chan string)

  

有緩衝通道

ch := make(chan string, 2)

  

2.2 兩類通道的區別

  1、從宣告方式來看,有緩衝帶了容量,即後面的數字,這裡的2表示通道可以存放兩個stirng型別的變數

  2、無緩衝通道本身不儲存資訊,它只負責轉手,有人傳給它,它就必須要傳給別人,如果只有進或者只有出的操作,都會造成阻塞。有緩衝的可以儲存指定容量個變數,但是超過這個容量再取值也會阻塞。

 

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 總結

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

2、通道分為無緩衝通道和有緩衝通道

3、通道使用時要注意是否構成死鎖以及各種死鎖產生的原因

 

如果您覺得閱讀本文對您有幫助,請點一下“推薦”按鈕,您的“推薦”將是我最大的寫作動力!如果您想持續關注我的文章,請掃描二維碼,關注JackieZheng的微信公眾號,我會將我的文章推送給您,並和您一起分享我日常閱讀過的優質文章。

 

&n