Golang通道Channel詳解

Gundy發表於2019-05-07

通道型別是Go自帶的,相當於是一個先進先出的佇列,同時唯一一個可以滿足併發安全性的型別。宣告一個通道型別變數的時候,首先需要確定通道型別的元素型別,然後還要確定通道的容量,當然預設容量是0。

初始化

使用make進行初始化,如下所示:

c := make(chan int)
c := make(chan string, 10)
複製程式碼

如果不指定容量,預設通道的容量是0,這種通道也成為非緩衝通道。

通道的傳送和接收特性

  1. 對於同一個通道,傳送操作之間是互斥的,接收操作之間也是互斥的。

    簡單來說就是在同一時刻,Go的執行系統只會執行對同一個通道的任意個傳送操作中的某一個,直到這個元素值被完全複製進該通道之後,其他傳送操作才會執行。針對接收操作也是這樣。 對於通道中的同一個值,傳送操作和接收操作也是互斥的。如正在被複制進通道但還未複製完成的元素值,這時接收方也不會看到和取走。

    tips 元素值從外界進入通道會被複制。也就是說進入通道的並不是在接收操作符右邊的那個元素值,而是他的副本。

  2. 傳送操作和接收操作中對元素值的處理都是不可分割的。 不可分割意思就是傳送操作要麼還沒複製元素,要麼已經複製完畢,不會出現值只複製了一部分的情況。

  3. 傳送操作在完全完成之前會被阻塞。接收操作也是如此。 傳送操作包括,“複製元素值”,“放置副本到通道內” 二個步驟。在這二個步驟完成之前,傳送操作會一直阻塞,他之後的程式碼是不會執行的。 接收操作包括“複製通道內元素值”,“放置副本到接收方”,“刪除原值” 三個操作。這三個操作在完成之前也是會一直阻塞的。

tips: 上面講的複製都屬於淺拷貝。淺拷貝只是拷貝值以及值中直接包含的東西,深拷貝就是把所有深層次的結構一併拷貝,Golang只有淺拷貝。

傳送操作和接收操作在什麼時候會被阻塞呢

對於快取通道

  • 如果通道已滿,所有的傳送操作就會阻塞,直到通道中有元素被取走
  • 如果通道已空,所有的接收操作就會阻塞,直到通道中有新的元素

對於非快取通道

  • 無論傳送操作還是接受操作一開始就是阻塞的,只有配對的操作出現才會開始執行。

收發操作何時會引起panic

  • 通道關閉,在進行傳送操作會引發panic

  • 關閉一個已經關閉的通道也會引發panic 更具體地說,當我們把接收表示式的結果同時賦給兩個變數時,第二個變數的型別就是一定bool型別。它的值如果為false就說明通道已經關閉,並且再沒有元素值可取了。

    注意,如果通道關閉時,裡面還有元素值未被取出,那麼接收表示式的第一個結果,仍會是通道中的某一個元素值,而第二個結果值一定會是true。因此,通過接收表示式的第二個結果值,來判斷通道是否關閉是可能有延時的。

    package main
    import "fmt"
    func main() {
    	ch1 := make(chan int, 2)
    	// 傳送方。
    	go func() {
    		for i := 0; i < 10; i++ {
    			fmt.Printf("Sender: sending element %v...\n", i)
    			ch1 <- i
    		}
    		fmt.Println("Sender: close the channel...")
    		close(ch1)
    	}()
    
    	// 接收方。
    	for {
    		elem, ok := <-ch1
    		if !ok {
    			fmt.Println("Receiver: closed channel")
    			break
    		}
    		fmt.Printf("Receiver: received an element: %v\n", elem)
    	}
    
    	fmt.Println("End.")
    }
    複製程式碼

Channel引起的死鎖的常見場景

死鎖是指兩個或兩個以上的協程的執行過程中,由於競爭資源或由於彼此通訊而造成的一種阻塞的現象,若無外力作用,他們將無法推進下去,解決死鎖的方法是加鎖。。結合上面講的channel相關知識,大家可以思考一下面情況為何為引起死鎖。

場景1:一個通道在一個go協程讀寫

func main() {
	c:=make(chan int)
	c<-666
	<-c
}
複製程式碼

場景二:go程開啟之前使用通道

func main() {
	c:=make(chan int)
	c<-666
	go func() {
		<-c
	}()
}
複製程式碼

場景三:通道1中呼叫了通道2,通道2中呼叫通道1

func main() {
	c1,c2:=make(chan int),make(chan int)
	go func() {
		for  {
			select{
				case <-c1:
					c2<-10
			}
		}
	}()
	for  {
		select{
		case <-c2:
			c1<-10
		}
	}	
}
複製程式碼

相關文章