go channel 關鍵特性解讀和示例

什麼是 goroutine

They're called goroutines because the existing terms — threads, coroutines, processes, and so on — convey inaccurate connotations. A goroutine has a simple model: it is a function executing in parallel with other goroutines in the same address space. It is lightweight, costing little more than the allocation of stack space. And the stacks start small, so they are cheap, and grow by allocating (and freeing) heap storage as required.

正如官方所言,goroutine 是一個輕量級的執行單元,相比線程開銷更小,完全由 Go 語言負責調度,是 Go 支持併發的核心。開啓一個 goroutine 非常簡單:

package main
import (
	"fmt"
	"time"
)

func main() {
	go fmt.Println("goroutine message")
	time.Sleep(1) //1
	fmt.Println("main function message")
}

#1 的代碼是必須的,這是爲了讓新開啓的 goroutine 有機會得到執行,開啓一個 goroutine 之後,後續的代碼會繼續執行,在上面的例子中後續代碼執行完畢程序就終止了,而開啓的 goroutine 可能還沒開始執行。

如果嘗試去掉 #1 處的代碼,程序也可能會正常運行,這是因爲恰巧開啓的 goroutine 只是簡單的執行了一次輸出,如果 goroutine 中耗時稍長就會導致只能看到主一句 main function message 。

換句話話說,這裏的 time.sleep 提供的是一種調度機制,這也是 Go 中 channel 存在的目的:負責消息傳遞和調度。

Channel

Channel 是 Go 中爲 goroutine 提供的一種通信機制,channel 是有類型的,而且是有方向的,可以把 channel 類比成 unix 中的 pipe。

i := make(chan int)//int 類型
s := make(chan string)//字符串類型
r := make(<-chan bool)//只讀
w := make(chan<- []int)//只寫

Channel 最重要的作用就是傳遞消息。

package main
import (
	"fmt"
)

func main() {
	c := make(chan int)
	go func() {
		fmt.Println("goroutine message")
		c <- 1 //1
	}()
	<-c //2
	fmt.Println("main function message")
}

例子中聲明瞭一個 int 類型的 channel,在 goroutine 中在代碼 #1 處向 channel 發送了數據 1 ,在 main 中 #2 處等待數據的接收,如果 c 中沒有數據,代碼的執行將發生阻塞,直到 c 中數據接收完畢。這是 channel 最簡單的用法之一:同步 ,這種類型的 channel 沒有設置容量,稱之爲 unbuffered channel。

unbuffered channel 和 buffered channel

Channel 可以設置容量,表示 channel 允許接收的消息個數,默認的 channel 容量是 0 稱爲 unbuffered channel ,對 unbuffered channel 執行 讀 操作 value := <-ch 會一直阻塞直到有數據可接收,執行 寫 操作 ch <- value 也會一直阻塞直到有 goroutine 對 channel 開始執行接收,正因爲如此在同一個 goroutine 中使用 unbuffered channel 會造成 deadlock。

package main
import (
	"fmt"
)

func main() {
	c := make(chan int)
	c <- 1
	<-c
	fmt.Println("main function message")
}

執行報 fatal error: all goroutines are asleep - deadlock! ,讀和寫相互等待對方從而導致死鎖發生。

unbuffered channel 圖解--來自 goinggo.net 的博客

如果 channel 的容量不是 0,此類 channel 稱之爲 buffered channel ,buffered channel 在消息寫入個數 未達到容量的上限之前不會阻塞 ,一旦寫入消息個數超過上限,下次輸入將會阻塞,直到 channel 有位置可以再寫入。

buffered channel 圖解--來自 goinggo.net 的博客

 

package main
import (
    "fmt"
)
	
func main() {
    c := make(chan int, 3)
    go func() {
	 for i := 0; i < 4; i++ {
	    c <- i
		fmt.Println("write to c ", i)
	}
    }()
	
    for i := 0; i < 4; i++ {
	fmt.Println("reading", <-c)
    }
}

上面的例子會輸出:

write to c 0
reading 0
write to c 1
reading 1
write to c 2
reading 2
write to c 3
reading 3

根據上文對 buffered channel 的解釋,這個例子中 channel c 的容量是 3,在寫入消息個數不超過 3 時不會阻塞,輸出應該是:

write to c 0
write to c 1
write to c 2
reading 0
reading 1
reading 2
write to c 3
reading 3

問題在哪裏?問題其實是在 fmt.Println ,一次輸出就導致 goroutine 的執行發生了切換(相當於發生了 IO 阻塞),因而即使 c 沒有發生阻塞 goroutine 也會讓出執行,一起來驗證一下這個問題。

package main
import (
	"fmt"
	"strconv"
)

func main() {
	c := make(chan int, 3)
	s := make([]string, 8)
	var num int = 0
	go func() {
		for i := 0; i < 4; i++ {
			c <- i
			num++
			v := "inner=>" + strconv.Itoa(num)
			s = append(s, v)
		}
	}()

	for i := 0; i < 4; i++ {
		<-c
		num++
		v := "outer=>" + strconv.Itoa(num)
		s = append(s, v)
	}

	fmt.Println(s)
}

這裏創建了一個 slice 用來保存 c 進行寫入和讀取時的執行順序,num 是用來標識執行順序的,在沒有加入 Println 之前,最終 s 是 [inner=>1 inner=>2 inner=>3 inner=>4 outer=>5 outer=>6 outer=>7 outer=>8] ,輸出結果表明 c 達到容量上線之後纔會發生阻塞。

相反有輸出語句的版本結果則不同:

package main
import (
	"fmt"
	"strconv"
)

func main() {
	c := make(chan int, 3)
	s := make([]string, 8)
	var num int = 0
	go func() {
		for i := 0; i < 4; i++ {
			c <- i
			num++
			v := "inner=>" + strconv.Itoa(num)
			s = append(s, v)
			fmt.Println("write to c ", i)
		}
	}()

	for i := 0; i < 4; i++ {
		num++
		v := "outer=>" + strconv.Itoa(num)
		s = append(s, v)
		fmt.Println("reading", <-c)
	}

	fmt.Println(s)
}

[outer=>1 inner=>2 outer=>3 inner=>4 inner=>5 inner=>6 outer=>7 outer=>8] 輸出結果能表明兩個 goroutine 是交替執行,也就是說 IO 的調用 Println 導致 goroutine 的讓出了執行。

讀取多個 channel 的消息

Go 提供了 select 語句來處理多個 channel 的消息讀取。

package main
import (
	"fmt"
	"time"
)

func main() {
	c1 := make(chan string)
	c2 := make(chan string)

	go func() {
		for {
			c1 <- "from 1"
			time.Sleep(time.Second * 2)
		}
	}()

	go func() {
		for {
			c2 <- "from 2"
			time.Sleep(time.Second * 2)
		}
	}()

	go func() {
		for {
			select {
			case msg1 := <-c1:
				fmt.Println(msg1)
			case msg2 := <-c2:
				fmt.Println(msg2)
			}
		}
	}()

	var input string
	fmt.Scanln(&input)

}

select 語句可以從多個可讀的 channel 中隨機選取一個執行,注意是 隨機選取。

Channel 關閉之後

Channel 可以被關閉 close ,channel 關閉之後仍然可以讀取,如果 channel 關閉之前有值寫入,關閉之後將依次讀取 channel 中的消息,讀完完畢之後再次讀取將會返回 channel 的類型的 zero value:

package main
import (
	"fmt"
)

func main() {
	c := make(chan int, 3)
	go func() {
		c <- 1
		c <- 2
		c <- 3
		close(c)
	}()

	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c)
	fmt.Println(<-c)
}

輸出 1 2 3 0 0 0 ,0 是 int channel c 的 zero value。

被關閉的 channel 可以進行 range 迭代:

package main
import (
	"fmt"
)

func main() {
	c := make(chan int, 3)
	go func() {
		c <- 1
		c <- 2
		c <- 3
		close(c)
	}()

	for i := range c {
		fmt.Println(i)
	}
}

未被關閉的 channel 則不行,如果沒有被關閉,range 在輸出完 channel 中的消息之後將會阻塞一直等待,從而發生死鎖。

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