Go 併發 -- 信道

這是『就要學習 Go 語言』系列的第 22 篇分享文章安全

上篇文章講了關於協程的一些用法,好比如何建立協程、匿名協程等。這篇文章咱們來說講信道。 信道是協程之間通訊的管道,從一端發送數據,另外一端接收數據。函數

信道聲明

使用信道以前須要聲明,有兩種方式:oop

var c chan int  		// 方式一
c := make(chan int)		// 方式二
複製代碼

使用關鍵字 chan 建立信道,聲明時有類型,代表信道只容許該類型的數據傳輸。信道的零值爲 nil。方式一就聲明瞭 nil 信道。nil 信道沒什麼做用,既不能發送數據也不能接受數據。方式二使用 make 函數建立了可用的信道 c。學習

func main() {
	c := make(chan int)
	fmt.Printf("c Type is %T\n",c)
	fmt.Printf("c Value is %v\n",c)
}
複製代碼

輸出:ui

c Type is chan int
c Value is 0xc000060060
複製代碼

上面的代碼建立了信道 c,並且只容許 int 型數據傳輸。通常將信道做爲參數傳遞給函數或者方法實現兩個協程之間通訊,有沒有注意到信道 c 的值是一個地址,傳參的時候直接使用 c 的值就能夠,而不用取址。spa

信道的使用

讀寫數據

Go 提供了語法方便咱們操做信道:.net

c := make(chan int)
// 寫數據
c <- data   

// 讀數據
variable <- c  // 方式一
<- c  			// 方式二
複製代碼

讀寫數據注意信道的位置,信道在箭頭的左邊是寫數據,在右邊是從信道讀數據。上面的方式二讀數據是合理的,讀出來的數據丟棄不使用。 注意:信道操做默認是阻塞的,往信道里寫數據以後當前協程便阻塞,直到其餘協程將數據讀出。一個協程被信道操做阻塞後,Go 調度器會去調用其餘可用的協程,這樣程序就不會一直阻塞。信道的這種特性很是有用,接下來咱們就能夠看到。 咱們來溫習下上篇文章的一個例子:code

func printHello() {
	fmt.Println("hello world goroutine")
}

func main() {
	go printHello()
	time.Sleep(1*time.Second)
	fmt.Println("main goroutine")
}
複製代碼

這個例子 main() 協程使用了 time.Sleep() 函數休眠了 1s 等待 printHello() 執行完成。很黑科技,在生產環境絕對不能夠這樣用。咱們使用信道修改下:cdn

func printHello(c chan bool) {
	fmt.Println("hello world goroutine")
	<- c    // 讀取信道的數據
}

func main() {
	c := make(chan bool)
	go printHello(c)
	c <- true    // main 協程阻塞
	fmt.Println("main goroutine")
}
複製代碼

輸出:協程

hello world goroutine
main goroutine
複製代碼

上面的例子,main 協程建立完 printHello 協程以後,第 8 行往信道 c 寫數據,main 協程阻塞,Go 調度器調度可以使用 printHello 協程,從信道 c 讀出數據,main 協程接觸阻塞繼續運行。注意:讀取操做沒有阻塞是由於信道 c 已有可讀的數據,不然,讀取操做會阻塞。

死鎖

前面提到過,讀/寫數據的時候信道會阻塞,調度器會去調度其餘可用的協程。問題來了,若是沒有其餘可用的協程會發生什麼狀況?沒錯,就會發生著名的死鎖。最簡單的狀況就是,只往信道寫數據。

func main() {
	c := make(chan bool)
	c <- true    // 只寫不讀
	fmt.Println("main goroutine")
}
複製代碼

報錯:

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

同理,只讀不寫也會報一樣的錯誤。

關閉信道與 for loop

發送數據的信道有能力選擇關閉信道,數據就不能傳輸。數據接收的時候能夠返回一個狀態判斷該信道是否關閉:

val, ok := <- channel
複製代碼

val 是接收的值,ok 標識信道是否關閉。爲 true 的話,該信道還能夠進行讀寫操做;爲 false 則標識信道關閉,數據不能傳輸。 使用內置函數 close() 關閉信道。

func printNums(ch chan int) {
	for i := 0; i < 4; i++ {
		ch <- i
	}
	close(ch)
}

func main() {
	ch := make(chan int)
	go printNums(ch)
	for {
		v, ok := <-ch
		if ok == false {     // 經過 ok 判斷信道是否關閉
			fmt.Println(v, ok)
			break
		}
		fmt.Println(v, ok)
	}
}
複製代碼

輸出:

0 true
1 true
2 true
3 true
0 false
複製代碼

printNums 協程寫完數據以後關閉了信道,在 main 協程裏對 ok 判斷,若爲 false 說明信道關閉,退出 for 循壞。從關閉的信道讀出來的值是對應類型的零值,上面最後一行的輸出值是 int 類型的零值 0。

使用 for 循環,須要手動判斷信道有沒有關閉。若是嫌煩的話,那就使用 for range 讀取信道吧,信道關閉,for range 自動退出。

func printNums(ch chan int) {
	for i := 0; i < 4; i++ {
		ch <- i
	}
	close(ch)
}

func main() {
	ch := make(chan int)
	go printNums(ch)

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

輸出:

0
1
2
3
複製代碼

提一點,使用 for range 一個信道,發送完畢以後必須 close() 信道,否則發生死鎖。

緩衝信道和信道容量

以前建立的信道是無緩衝的,讀寫信道會立馬阻塞當前協程。對於緩衝信道,寫不會阻塞當前信道直到信道滿了,同理,讀操做也不會阻塞當前信道除非信道沒數據。建立帶緩衝的信道:

ch := make(chan type, capacity)  
複製代碼

capacity 是緩衝大小,必須大於 0。 內置函數 len()、cap() 能夠計算信道的長度和容量。

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

	ch <- 7
	ch <- 8
	ch <- 9
	//ch <- 10 
	// 註釋打開的話,協程阻塞,發生死鎖
	會發生死鎖:信道已滿且沒有其餘可用信道讀取數據

	fmt.Println("main stopped")
}
複製代碼

輸出:main stopped 建立了緩衝爲 3 的信道,寫入 3 個數據時信道不會阻塞。若是將第 7 行代碼註釋打開的話,此時信道已滿,協程阻塞,又沒有其餘可用協程讀數據,便發生死鎖。 再來看個例子:

func printNums(ch chan int) {

	ch <- 7
	ch <- 8
	ch <- 9
	fmt.Printf("channel len:%d,capacity:%d\n",len(ch),cap(ch))
	fmt.Println("blocking...")
	ch <- 10   // 阻塞
	close(ch)
}

func main() {
	ch := make(chan int,3)
	go printNums(ch)

	// 休眠 2s
	time.Sleep(2*time.Second)
	for v := range ch {
		fmt.Println(v)
	}

	fmt.Println("main stopped")
}
複製代碼

輸出:

channel len:3,capacity:3
blocking...
7
8
9
10
main stopped
複製代碼

休眠 2s 的目的是讓信道寫滿數據發生阻塞,從打印結果能夠看出。2s 以後,主協程從信道讀取數據,信道容量有餘阻塞便解除,繼續寫數據。

若是緩衝信道是關閉狀態但有數據,仍然能夠讀取數據:

func main() {
	ch := make(chan int,3)
	
	ch <- 7
	ch <- 8
	//ch <- 9
	close(ch)

	for v := range ch {
		fmt.Println(v)
	}

	fmt.Println("main stopped")
}
複製代碼

輸出:

7
8
main stopped
複製代碼

單向信道

以前建立的都是雙向信道,既能發送數據也能接收數據。咱們還能夠建立單向信道,只發送或者只接收數據。 語法:

sch := make(chan<- int)
rch := make(<-chan int) 
複製代碼

sch 是隻發送信道,rch 是隻接受信道。 這種單向信道有什麼用呢?咱們總不能只發不接或只接不發吧。這種信道主要用在信道做爲參數傳遞的時候,Go 提供了自動轉化,雙向轉單向。 重寫以前的例子:

func printNums(ch chan<- int) {
	for i := 0; i < 4; i++ {
		ch <- i
	}
	close(ch)
}

func main() {
	ch := make(chan int)
	go printNums(ch)

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

輸出:

0
1
2
3
複製代碼

main 協程中 ch 是一個雙向信道,printNums() 在接收參數的時候將 ch 自動轉成了單向信道,只發不收。但在 main 協程中,ch 仍然能夠接收數據。使用單向通道主要是能夠提升程序的類型安全性,程序不容易出錯。

信道數據類型

信道是一類值,相似於 int、string 等,能夠像其餘值同樣在任何地方使用,好比做爲結構體成員、函數參數、函數返回值,甚至做爲另外一個通道的類型。咱們來看下使用通道做爲另外一個通道的數據類型

func printWord(ch chan string) {
	fmt.Println("Hello " + <-ch)
}

func productCh(ch chan chan string)  {
	c := make(chan string)   // 建立 string type 信道
	ch <- c     // 傳輸信道
}

func main() {

	// 建立 chan string 類型的信道
	ch := make(chan chan string)
	go productCh(ch)
	// c 是 string type 的信道
	c := <-ch
	go printWord(c)
	c <- "world"
	fmt.Println("main stopped")
}
複製代碼

輸出:

Hello world
main stopped
複製代碼

但願這篇文章可以幫助你,Good day!


(全文完)

原創文章,若需轉載請註明出處!
歡迎掃碼關注公衆號「Golang來啦」或者移步 seekload.net ,查看更多精彩文章。

給你準備了學習 Go 語言相關書籍,公號後臺回覆【電子書】領取!

公衆號二維碼
相關文章
相關標籤/搜索