天天看點

Go channel [golang學習筆記5]1.Go 對 CSP 的實作2. goroutine的實作3. channel的實作

Go channel [golang學習筆記5]

  • 1.Go 對 CSP 的實作
    • goroutine
    • channel
  • 2. goroutine的實作
  • 3. channel的實作
    • 不帶緩沖區:
    • 帶緩沖區:
    • 管道,串聯的channel(Pipeline)
    • 單向通道類型
    • 注意:常見的幾個goroutine死鎖
    • select多路複用

1.Go 對 CSP 的實作

要想了解 goalng channel 要先知道 CSP 模型。CSP 是 Communicating Sequential Process 的簡稱,中文可以叫做通信順序程序,是一種并發程式設計模型。

Go 實作了兩種并發形式。第一種是大家普遍認知的:多線程共享記憶體。其實就是 Java 或者 C++ 等語言中的多線程開發。

另外一種是Go語言特有的,也是Go語言推薦的:CSP 并發模型,不同于傳統的多線程通過共享記憶體來通信,CSP 講究的是“以通信的方式來共享記憶體”。Go 的 CSP 并發模型,是通過 goroutine 和 channel 來實作的。

goroutine

在go裡面,每一個并發執行的活動成為goroutine。通過go語句進行建立。

goroutine(協程) 是Go語言中并發的執行機關。goroutine 可以認為是輕量級的線程,與建立線程相比,建立成本和開銷都很小,每個goroutine 的堆棧隻有幾kb,并且堆棧可根據程式的需要增長和縮小(線程的堆棧需指明和固定),是以go程式從語言層面支援了高并發。

channel

channel 是 Go 語言中各個并發結構體( goroutine )之前的通信機制。 通俗的講,就是各個 goroutine 之間通信的 “管道”。

CSP 模型的關鍵是關注 channel,而不關注發送消息的實體。Go 語言實作了 CSP 部分理論,goroutine 對應 CSP 中并發執行的實體,channel 也就對應着 CSP 中的 channel。

2. goroutine的實作

在函數或者方法前面加上關鍵字go,即建立一個并發運作的新goroutine。

func main() {
	go func() {
		fmt.Println("Hello world!")
	}()
	time.Sleep(2 * time.Second)
}
           

需要注意的是,執行速度很快,一定要加 sleep,避免 goroutine 未執行, main 方法就結束了,這樣就看不到goroutine裡的 “Hello world!” 輸出。

這也說明了一個關鍵點:當main函數傳回時,所有的gourutine都是暴力終結的,然後程式退出。

3. channel的實作

關于關閉 channel 有幾點需要注意的是:

重複關閉 channel 會導緻 panic。

向關閉的 channel 發送資料會 panic。

從關閉的 channel 讀資料不會 panic,讀出 channel 中已有的資料之後再讀就是 channel 類似的預設值,比如 chan int 類型的 channel 關閉之後讀取到的值為 0。

go channel 有兩種,一種是帶緩沖區,一種是不帶緩沖區的。

無緩沖:發送和接收動作是同時發生的。如果沒有 goroutine 讀取 channel (<- channel),則發送者 (channel <-) 會一直阻塞。

緩沖:緩沖 channel 類似一個有容量的隊列。當隊列滿的時候發送者會阻塞;當隊列空的時候接收者會阻塞。直接上例子。

不帶緩沖區:

func test1() {
	fmt.Println("make chan")
	done := make(chan bool) //不設定緩沖
	fmt.Println("goroutine")
	go func() {
		fmt.Println("Hello world goroutine")
		time.Sleep(1 * time.Second)
		fmt.Println("done <- true")
		done <- true
	}()
	fmt.Println("<-done") //被阻塞
	fmt.Println("return")
}
           

無緩沖通道上的發送操作将會被阻塞,直到另一個goroutine在對應的通道上執行接收操作,此時值才傳送完成,程式繼續執行。

帶緩沖區:

func main() {
	messages := make(chan string, 2) //設定緩沖大小
	go func() {
		fmt.Println("ping")
		messages <- "ping"
		fmt.Println("pong")
		messages <- "pong"
		fmt.Println("ping pong")
		messages <- "ping pong" // goroutine會在這裡阻塞
	}()
	fmt.Println("1", <-messages)
	fmt.Println("2", <-messages)
	fmt.Println("3", <-messages)
}
           

goroutine 向 channel 發送資料的時候如果緩沖還沒滿,那麼該goroutine 就不會阻塞。反之,如果接受該 channel 資料的時候,如果緩沖有資料,那麼該 goroutine 就不會阻塞。

管道,串聯的channel(Pipeline)

channel 也可以用于将多個 goroutine 連結在一起,一個 channel 的輸出作為下一個 channel 的輸入。這種串聯的 channel 就是所謂的管道(pipeline)。

func main() {
	echo := make(chan string)
	receive := make(chan string)

	go func() {
		time.Sleep(1 * time.Second)
		echo <- "echo test"
	}()

	go func() {
		temp := <-echo
		receive <- temp
	}()
	fmt.Println(<-receive)
	//在這裡不一定要去關閉channel,因為底層的垃圾回收機制會根據它是否可以通路來決定是否自動回收它。
	//(這裡不是根據channel是否關閉來決定的)
}
           

單向通道類型

當程式則夠複雜的時候,為了代碼可讀性更高,拆分成一個一個的小函數是需要的此時go提供了單向通道的類型,來實作函數之間channel的傳遞。

使用 channel 來使不同的goroutine去進行通信,很多時候都和消費者生産者模式很相似,一個 goroutine 生産的結果都用 channel 傳送給另一個 goroutine,一個 goroutine 的執行依賴與另一個 goroutine 的結果。

是以很多情況下,channel 都是單方向的,在 go 裡面可以把一個無方向的 channel 轉換為隻接受或者隻發送的 channel,但是卻不能反過來把接受或發送的 channel 轉換為無方向的 channel,适當地把 channel 改成單方向,可以達到程式強限制的做法。

單向通道和雙向通過的差別:

類型 chan<- string 表示一個隻發送 string 的 channel,隻能發送不能接收

相反,類型 <-chan int 表示一個隻接收 string 的channel,隻能接收不能發送

(箭頭<-和關鍵字chan的相對位置表明了channel的方向。)這種限制将在編譯期檢測

func main() {
	echo := make(chan string)
	receive := make(chan string)

	go func(out chan<- string) {
		time.Sleep(1 * time.Second)
		echo <- "echo test"
		close(out)
	}(echo)
	//out chan<- string 單向通道,隻用于寫(發送) string 類型資料
	//in <-chan string 單向通道,隻用于讀取(接收) string 類型資料
	go func(out chan<- string, in <-chan string) {
		temp := <-in //阻塞等待echo的通道的傳回
		out <- temp
		close(out)
	}(receive, echo)
	fmt.Println(<-receive)
}
           

注意:常見的幾個goroutine死鎖

①如果使用 channel 之前沒有 make,會出現 dead lock 錯誤,如下:

func main() {
	var str chan string
	go func() {
		str <- "Try a test."
	}()
	fmt.Println(<-str)
}
           

錯誤消息:

fatal error: all goroutines are asleep - deadlock!
           

②沒有建立 goroutine, 直接使用 無緩沖channel ,會出現 dead lock 錯誤

func main() {
	echo := make(chan string)
	echo <- "echo test"
	fmt.Println(<-echo)
}
           

③沒有建立 goroutine, 直接使用 緩沖channel ,超出緩沖時被阻塞,會出現 dead lock 錯誤

func main() {
	echo := make(chan string, 1)
	echo <- "echo test1"
	echo <- "echo test2" //被阻塞
	fmt.Println(<-echo)
}
           

select多路複用

在一個goroutine裡面,對channel的操作很可能導緻我們目前的goroutine阻塞,而我們之後的操作都進行不了。而如果我們又需要在目前channel阻塞進行其他操作,如操作其他channel或直接跳過阻塞,可以通過select來達到多個channel(可同時接受和發送)複用。

下面給個簡單例子:

func main() {
	event := make(chan string)
	timeout := make(chan bool, 1)
	go func() {
		time.Sleep(1 * time.Second) // 休眠1s,如果超過1s還沒I操作則認為逾時,通知select已經逾時啦~
		timeout <- true
	}()

	go func() {
		event <- "go happens"
	}()

DONE:
	for {
		select {
		case <-event:
			fmt.Println("happens!")
			//goto DONE
			break DONE
		case <-timeout:
			fmt.Println("Timeout!")
		default:
			fmt.Println("No news, please wait.")
			time.Sleep(2 * time.Second)
		}
	}
//DONE:
	fmt.Println("return!")
}