golang如何更好地使用channel

最近學習了《GO語言併發之道》這本書,獲益匪淺,其中channel方面的知識瞭解了更多,主要是以下幾點:

1. channel在不同條件下讀寫,會有不同的行爲形式,後面會通過實驗說明;

2. channel使用完是要close的,而一般由寫端創建和關閉,不要在讀端關閉,上面的實驗結果會說明這樣做的原因;

3. channel結合gorouting有很多的實踐方式,還可以構造流式處理。

先來看看實驗代碼,首先看看讀channel的代碼

package read

import "fmt"

// 阻塞
func Nil() {
	var ch chan interface{}
	<-ch
}

// 阻塞
func Empty() {
	ch := make(chan interface{})
	<-ch
}

// 阻塞
func CloseNotEmpty() {
	ch := make(chan interface{})
	ch <- 10
	close(ch)
	fmt.Println(<-ch)
}

// 讀取0值
func CloseEmpty() {
	ch := make(chan interface{})
	close(ch)
	fmt.Println(<-ch)
}

函數主要包括如下內容,實驗結果已經在代碼中進行了註釋:

Nil:讀取nil channel

Empty:讀取沒有數據的channel

CloseNotEmpty:讀取被關閉的非空channel

CloseEmpty:讀取被關閉的空channel

然後是寫channel的代碼

package write

// 阻塞
func Nil() {
	var ch chan interface{}
	ch <- 10
}

// 阻塞
func Full() {
	ch := make(chan interface{}, 1)
	ch <- 10
	ch <- 11
}

// 可寫入
func NotFull() {
	ch := make(chan interface{}, 1)
	ch <- 10
}

// panic send on closed channel
func Closed() {
	ch := make(chan interface{}, 1)
	close(ch)
	ch <- 10
}

函數說明如下,實驗結果也已經在註釋中給出:

Nil:向nil channel寫入數據

Full:向填滿的channel寫入數據

NotFull:向未填滿的channel寫入數據

Closed:向已經關閉的channel寫入數據

最後是實驗關閉channel的函數:

package close

//panic: close of nil channel
func Nil() {
	var ch chan interface{}
	close(ch)
}

//panic: close of closed channel
func Closed() {
	ch := make(chan interface{})
	close(ch)
	close(ch)
}

函數說明如下,結果註釋已給出:

Nil:關閉nil channel

Closed:關閉已經關閉的channel

從上面的實驗結果可以看出,如果從讀端關閉channel,寫端去寫關閉的channel時,會出現panic,導致整個程序終止。而從寫端關閉channel,讀取時,返回0值或阻塞,至少有跡可循,方便我們調試代碼。

下面給出了結合channel如何編寫比較好的gorouting的一個例子

package main

import (
	"fmt"
	"strings"
	"time"
)

func toUpper(done <-chan interface{}, str string) <-chan string {
	strChan := make(chan string)

	go func() {
		defer close(strChan)

		for {
			select {
			case <-done:
				return
			case strChan <- strings.ToUpper(str):
			}
		}
	}()

	return strChan
}

func main() {
	done := make(chan interface{})
	defer close(done)

	toUpperChan := toUpper(done, "aaBBcc")
	fmt.Println(<-toUpperChan)
}

toUpper函數接受兩個參數,第一個參數是一個可讀channel,用來終止協程,str是需要轉換爲大寫的字符串。返回值是一個可讀的channel,這個channel作爲協程返回數據的媒介,如果用過Java,也可以理解爲這個channel類似於Future對象,也可以理解爲是協程的join點。toUpper函數邏輯比較簡單,一個比較大的select,讀取done channel並向strChan寫入大寫轉換後的字符串。遵循上面提到的原則,作爲strChan的寫端,toUpper函數創建strChan,並通過defer延遲執行,在函數return後,關閉strChan。

主函數創建done channel並傳遞給toUpper,當主函數退出時,通過關閉done channel,toUpper函數中將從done讀取到nil,從而退出協程函數。主函數調用toUpper函數,通過返回的channel,讀取處理後的字符串。

可以看到,採用這種方式,使得協程的管理更加簡單,還能保證channel正常關閉,取得協程返回值,避免了channel和gorouting的泄露。下面這段代碼,更加華麗

package main

import (
	"fmt"
	"strings"
	"time"
)

func str(done <-chan interface{}, str string) <-chan string {
	strChan := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)

		defer close(strChan)

		select {
		case <-done:
			return
		case strChan <- str:
		}
	}()

	return strChan
}

func toUpper(done <-chan interface{}, str <-chan string) <-chan string {
	strChan := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)

		defer close(strChan)

		for {
			select {
			case <-done:
				return
			case strChan <- strings.ToUpper(<-str):
			}
		}
	}()

	return strChan
}

func appendStr(done <-chan interface{}, oldStr <-chan string, appendStr string) <-chan string {
	strChan := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)

		defer close(strChan)

		for {
			select {
			case <-done:
				return
			case strChan <- <-oldStr + appendStr:
			}
		}
	}()

	return strChan
}

func main() {
	done := make(chan interface{})

	strChan := str(done, "aaa111bbb222CCC")
	fmt.Println(1)

	upperChan := toUpper(done, strChan)
	fmt.Println(2)

	appendStrChan := appendStr(done, upperChan, "dddd")
	fmt.Println(3)

	fmt.Println(<-appendStrChan)

	close(done)

	done = make(chan interface{})
	fmt.Println(<-appendStr(done, toUpper(done, str(done, "aaa111bbb222CCC")), "dddd"))
	close(done)
}

有三個函數,str,toUpper和appendStr,這三個函數都採用了上面的模式,不同的是toUpper和appendStr這兩個函數的第二個輸入參數使用了一個可讀channel,這樣,我們可以構造一個異步的流式處理流程,str作爲這個流處理的入口,參數使用原生類型,toUpper和appendStr作爲流處理的處理節點,使用通道作爲參數。

main函數中給出了兩種組裝流處理流程的方式,第一種方式,通過調用每一個函數,然後將返回的channel作爲下一個處理節點輸入進行傳參,中間添加了打印,運行時可以看到,函數調用是立刻返回的,在最後獲取結果時進行等待;第二種方式更加簡潔,基於函數式編程構造了流處理。

關閉done channel可以起到廣播的作用,所有等待在讀取done的協程,都會讀到nil,從而保證流處理過程中使用的所有協程都能退出。

golang的高併發主要靠channel和gorouting實現,深刻理解兩者的使用方式,將有利於我們寫出更高效的golang代碼。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章