Go語言基礎之併發(channel通信)

channel

單純地將函數併發執行是沒有意義的。函數與函數間需要交換數據才能體現併發執行函數的意義。

雖然可以使用共享內存進行數據交換,但是共享內存在不同的goroutine中容易發生競態問題。爲了保證數據交換的正確性,必須使用互斥量對內存進行加鎖,這種做法勢必造成性能問題。
Go語言的併發模型是CSP(Communicating Sequential Processes),提倡通過通信共享內存而不是通過共享內存而實現通信。

Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要爲其指定元素類型。

channel是一種類型,一種引用類型。

channel操作

通道有發送(send)、接收(receive)和關閉(close)三種操作。
發送和接收都使用<-符號。

向管道發送數據

ch <- 10 // 把10發送到ch中

從管道接受數據

x := <- ch // 從ch中接收值並賦值給變量x
//<-ch       // 從ch中接收值,忽略結果

關閉管道

close(ch)

關於關閉通道需要注意的事情是,只有在通知接收方goroutine所有的數據都發送完畢的時候才需要關閉通道。通道是可以被垃圾回收機制回收的,它和關閉文件是不一樣的,在結束操作之後關閉文件是必須要做的,但關閉通道不是必須的。

關閉後的通道有以下特點:

  1. 對一個關閉的通道再發送值就會導致panic。
  2. 對一個關閉的通道進行接收會一直獲取值直到通道爲空。
  3. 對一個關閉的並且沒有值的通道執行接收操作會得到對應類型的零值。
  4. 關閉一個已經關閉的通道會導致panic。

媽媽知道我學習累了,允許我玩半小時遊戲。半小時之後她通過channel告訴我,你該睡覺了。
女朋友在和我進行了十個來回的對話之後,發現了可能是機器人,她不再回復我了。

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

//定義一個計數器對goroutine進行計數,main函數結束前,保證其它goroutine都已經執行結束
var wg sync.WaitGroup

func chat(who string) {
	defer wg.Done()
	for i := 0; i < 10; i++ { //女朋友聊十句話,發現了可能是機器人,睡覺了
		n := rand.Int31n(5) //生成[0,5)的隨機數
		switch n {
		case 0:
			fmt.Println(who, ",我想你了,你在幹嘛?")
		case 1:
			fmt.Println(who, ",我想你了,我在和小可愛聊天呀")
		case 2:
			fmt.Println(who, ",你今天有什麼有趣的跟我分享嗎?")
		case 3:
			fmt.Println(who, ",哇啊,還是我的小可愛機智過人,比心")
		case 4:
			fmt.Println(who, ",親愛的,你真漂亮")
		default:
			fmt.Println(who, ",I love you!!!")
		}
		time.Sleep(time.Second)
	}
}

func countdown(chWithMa chan bool) {
	defer wg.Done() //告訴計數器,我的任務完成了,我要退出了
	time.Sleep(time.Minute * 30)
	chWithMa <- false
	close(chWithMa)
}

func main() {
	who := "girlFriend"
	chWithMa := make(chan bool)
	wg.Add(2)
	go countdown(chWithMa)
	go chat(who) //聊天機器人去聊天
	//我現在正在玩遊戲,我時刻聽着母親的呼喊
	<-chWithMa //這裏會阻塞一分鐘,這一分鐘是我的自由時間
	close(chWithMa)
	wg.Wait()  //等待計數器爲0,才終止程序
}

上述啓動了兩個goroutine,main函數如果不對自己執行流啓動的goroutine進行統計,它盲目退出,有可能導致正在工作的goroutine被迫因爲進程的退出的停止,爲了讓所有goroutine在完成工作之前,main函數都不退出,需要使用sync.WaitGroup來實現goroutine的同步。

有緩衝通道和無緩衝通道

無緩衝的通道又稱爲阻塞的通道。ch := make(chan int)
使用無緩衝通道進行通信將導致發送和接收的goroutine同步化。因此,無緩衝通道也被稱爲同步通道。
無緩衝通道上的發送操作會阻塞,直到另一個goroutine在該通道上執行接收操作,這時值才能發送成功,兩個goroutine將繼續執行。

package main

import (
	"fmt"
)

func recv(c chan int) {
	ret := <-c
	fmt.Println("接收成功", ret)
}
func main() {
	ch := make(chan int)
	go recv(ch) // 啓用goroutine從通道接收值
	ch <- 10
	fmt.Println("發送成功")
}

有緩衝通道ch := make(chan int,3)指定了通道容量。

package main

import (
	"fmt"
)

func main() {
	ch := make(chan int, 1) // 創建一個容量爲1的有緩衝區通道
	ch <- 10
	fmt.Println("發送成功")
}

只要通道的容量大於零,那麼該通道就是有緩衝的通道,通道的容量表示通道中能存放元素的數量。就像你小區的快遞櫃只有那麼個多格子,格子滿了就裝不下了,就阻塞了,等到別人取走一個快遞員就能往裏面放一個。

我們可以使用內置的len函數獲取通道內元素的數量,使用cap函數獲取通道的容量,雖然我們很少會這麼做。

從通道取值

從通道取值,還可以判斷通道是否已經關閉,x := <- ch 可以接受兩個值val,ok:=<-ch第二個值是bool類型。

package main

import (
	"fmt"
	"strconv"
)

// channel 練習
func main(){
	chat:=make(chan string)
	go func(){
		//往chat中寫入數據
		for i:=0;i<100;i++{
			chat<-strconv.Itoa(i)
		}
		close(chat)
	}()
	for {
		val,ok:=<-chat
		if !ok{
			break
		}else{
			fmt.Println(val)
		}
	}
}

實際上還可以通過for range方式去取值,而不用判斷通道是否關閉。

for val:=range chat{
		fmt.Println(val)
	}

單向通道

有的時候我們會將通道作爲參數在多個任務函數間傳遞,很多時候我們在不同的任務函數中使用通道都會對其進行限制,比如限制通道在函數中只能發送或只能接收。

Go語言中提供了單向通道來處理這種情況。

package main

import (
	"fmt"
	"strconv"
)

func send(chat chan<- string){//send函數對於通道chat只寫權限,不能從chat取值
	//往chat中寫入數據
	for i:=0;i<100;i++{
		chat<-strconv.Itoa(i)
	}
	close(chat)
}
func recv(chat <-chan string){//recv函數對於chat只讀權限,不能向chat發送值
	for val:=range chat{
		fmt.Println(val)
	}
}
func main(){
	chat:=make(chan string)
	go send(chat)
	go recv(chat)
}

通道總結
channel常見的異常總結,如下圖:
在這裏插入圖片描述

select多路複用

在某些場景下我們需要同時從多個通道接收數據。通道在接收數據時,如果沒有數據可以接收將會發生阻塞。

select的使用類似於switch語句,它有一系列case分支和一個默認的分支。每個case會對應一個通道的通信(接收或發送)過程。select會一直等待,直到某個case的通信操作完成時,就會執行case分支對應的語句。具體格式如下:

select{
    case <-ch1:
        ...
    case data := <-ch2:
        ...
    case ch3<-data:
        ...
    default:
        //默認操作
}
package main

//作爲新時代的好男人,既要聽老婆的話也要孝順聽父母的
import (
	"fmt"
	"strconv"
	"sync"
	"time"
)

var wg sync.WaitGroup

func parent(mather chan<- string) {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		msg1 := "父母都是爲你好"
		msg2 := strconv.Itoa(i)
		msg := fmt.Sprintf("%s%s", msg2, msg1)
		mather <- msg
	}
	close(mather)
}

func woman(wife chan<- string) {
	defer wg.Done()
	for i := 0; i < 10; i++ {
		msg1 := "老婆纔是陪着你終老的人"
		msg2 := strconv.Itoa(i)
		msg := fmt.Sprintf("%s%s", msg2, msg1)
		wife <- msg
	}
	close(wife)
}
func man(wife, mather <-chan string) {
	defer wg.Done()
	wifeOver := true
	matherOver := true
	for wifeOver || matherOver {
		select {
		case x, ok := <-wife:
			if ok {
				fmt.Println(x)
			} else {
				wifeOver = false
			}
		case y, ok := <-mather:
			if ok {
				fmt.Println(y)
			} else {
				matherOver = false
			}
		default:
			fmt.Println("今天星期天")
		}
		time.Sleep(time.Second)
	}
}

func main() {
	wife := make(chan string)
	mather := make(chan string, 3)
	wg.Add(3)
	go parent(mather)
	go woman(wife)
	go man(wife, mather)
	wg.Wait()
}

使用select語句能提高代碼的可讀性。

  • 可處理一個或多個channel的發送/接收操作。
  • 如果多個case同時滿足,select會隨機選擇一個。
  • 對於沒有case的select{}會一直等待,可用於阻塞main函數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章