goroutine
使用golang的channel之前,我們需要先了解go的goroutine。
Go 語言支持並發,我們只需要通過 go 關鍵字來開啟 goroutine 即可。
goroutine 是輕量級線程,相比線程開銷更小,完全由 Go 語言負責調度,是 Go 支持並發的核心。
如下所示,在go中我們可以很方便的開啟並發執行。
package main
import (
"fmt"
"time"
)
func main() {
go fmt.Println("goroutine message")
fmt.Println("main function message")
time.Sleep(time.Second) //休眠1s
}
channel
通道(channel)則是用來傳遞數據的一個數據結構。 大部分時候 channel 都是和 goroutine 一起配合使用。
通道可用於兩個 goroutine 之間通過傳遞一個指定類型的值來同步運行和通訊。操作符 <- 用於指定通道的方向,發送或接收。如果未指定方向,則為雙向通道。
chan T // 可以接收和發送類型為 T 的數據, 定義時使用
chan<- float64 // 只可以用來發送 float64 類型的數據, 在函數參數中使用, 這樣可以限定chan使用
<-chan int // 只可以用來接收 int 類型的數據, 在函數參數中使用, 這樣可以限定chan使用
無緩沖channel
- 我們可以使用如下的方式聲明一個無緩沖區的channel。其中int代表這個通道傳遞的是int類型。除了int、string、float等基本類型外,channel傳遞的類型還可以是自定義的結構體或別名等。
c := make(chan int) //聲明一個int類型的無緩沖通道
type NewType uint8
c1 := make(chan NewType) //聲明自定義類型的無緩沖通道
- 通道最基本的用法就是在多個協程之間傳遞消息。channel是線程安全的,即在使用過程中,有多個協程同時向一個channel發送數據,或讀取數據是完全可行的,不需要額外的操作。
- 無緩沖的通道只有當發送方和接收方都准備好時才會傳送數據,否則准備好的一方將會被阻塞。
- 我們來看如下這個例子:我們聲明了一個無緩沖的channel,然后開啟一個協程向這個channel發送數據,另外主線程則從這個channel中讀取數據。我們觀察程序的輸出可以發現,在主線程休眠期間,協程是阻塞在發送向通道發送數據的地方,只有當主線程休眠結束開始從channel中讀取數據時,協程才開始向下運行。同樣的,當協程發送完第一個數據休眠時,主線程讀取了第一個數據,准備從channel中讀取第二個數據時會被阻塞,知道協程休眠結束向通道發送數據后才會繼續運行。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) //聲明一個int類型的無緩沖通道
go func() {
fmt.Println("ready to send in g1")
c <- 1
fmt.Println("send 1 to chan")
fmt.Println("goroutine start sleep 1 second")
time.Sleep(time.Second)
fmt.Println("goroutine end sleep")
c <- 2
fmt.Println("send 2 to chan")
}()
fmt.Println("main thread start sleep 1 second")
time.Sleep(time.Second)
fmt.Println("main thread end sleep")
i := <- c
fmt.Printf("receive %d\n", i)
i = <- c
fmt.Printf("receive %d\n", i)
time.Sleep(time.Second)
}
輸出:
- 由於channel這種阻塞發送方和接收方的特性,所以我們在使用channel時要防止死鎖的發生。很明顯,如果我們在一個線程內向同一個channel同時進行讀取和發送的操作,就會導致死鎖。
package main
import (
"fmt"
)
func main() {
c := make(chan int) //聲明一個int類型的無緩沖通道
c <- 1
i := <- c
fmt.Printf("receive %d\n", i)
}
有緩存的channel
- 我們可以通過如下方式聲明一個有緩存的channel。有緩存的channel區別在於只有當緩沖區被填滿時,才會阻塞發送者,只有當緩沖區為空時才會阻塞接受者。
c := make(chan int, 10)
- 觀察如下的例子:我們聲明了一個容量為2的有緩沖的channel。開啟一個協程,這個協程會向這個channel連續發送4個數據,然后休眠5s,接着再向channel發送2個數據。而主線程則會從這個channel中讀取數據,每次讀取前會先休眠1s。通過觀察程序輸出,我們可以發現,協程首先向channel發送了2個數據后(0、1),被阻塞,因為這時主線程在進行1s的休眠。主線程休眠結束后,從channel中讀取了第一個數據0,之后繼續休眠1s。channel此時的又有了緩沖,於是協程又向channel發送了第三個數據2,而后再次因為channel的緩沖區已滿而休眠。依次類推,直到協程將4個數據發送完成之后,開始進行了5s的休眠。而當主線程從channel讀完第4個數據(3)之后,當准備再從channel中讀取第五個數據時,由於channel為空,主線程作為接受者被阻塞。直到協程的5s休眠結束,再次向channel中發送數據后,主線程讀取到數據而不被阻塞。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 2) //聲明一個int類型的有緩沖通道
go func() {
for i := 0; i < 4; i++ {
c <- i
fmt.Printf("send %d\n", i)
}
time.Sleep(5 * time.Second)
for i := 4; i < 6; i++ {
c <- i
fmt.Printf("send %d\n", i)
}
}()
for i := 0; i < 6; i++ {
time.Sleep(time.Second)
fmt.Printf("receive %d\n", <-c)
}
}
輸出:
關閉一個channel
- 我們可以使用close關鍵字關閉一個channel,如下所示。關閉channel時我們要注意一些細節。
c := make(chan int, 2)
close(c)
- 關閉channel的操作原則上應該由發送者完成,因為如果仍然向一個已關閉的channel發送數據,會導致程序拋出panic。而如果由接受者關閉channel,則會遇到這個風險。
package main
func main() {
c := make(chan int, 2)
close(c)
c <- 1
}
- 從一個已關閉的channel中讀取數據不會報錯。只不過需要注意的是,接受者就不會被一個已關閉的channel的阻塞。而且接受者從關閉的channel中仍然可以讀取出數據,只不過是這個channel的數據類型的默認值。我們可以通過指定接受狀態位來觀察接受的數據是否是從一個已關閉的channel所發送出來的數據。例如
j, ok := <-c
,則ok為false時,則代表channel已經被關閉。 - 如下圖這個例子,在協程關閉channel之前,主線程仍然會被這個協程所阻塞,而且讀取數據時,注意狀態位是true。當協程關閉channel之后,主線程仍然可以從channel中讀取出int的默認值0,只不過狀態變量變為了false,而且不再被阻塞,直到循環結束。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 2)
go func() {
c <- 1
time.Sleep(time.Second)
c <- 2
time.Sleep(time.Second)
close(c)
}()
for i := 0; i < 6; i++ {
j, ok := <-c
fmt.Printf("receive: %d, status: %t\n", j, ok)
}
}
和select關鍵字及for循環配合使用
for range 語法
- 我們可以使用for循環,持續的從一個channel中接受數據,當channel為空時,for循環會被阻塞。當channel被關閉時,則會跳出for循環。
- 如下例子,協程向channel中循環發送數據,並在循環結束時關閉channel。主線程是使用for range語句從channel中讀取數據,很明顯可以觀察到,當channel為空時,for循環會被阻塞,當channel為無緩沖的時候也是如此。當協程關閉channel后,主線程跳出了for循環。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 3; i++ {
c <-i
fmt.Printf("send %d\n", i)
time.Sleep(time.Second)
}
fmt.Println("ready close channel")
close(c)
}()
for i := range c {
fmt.Printf("receive %d\n", i)
}
fmt.Println("quit for loop")
}
select語法
- 使用select語句可以在多個可供選擇的channel中讀取任意一個數據執行。如果沒有任何一個channel可以讀取數據,則線程會被阻塞住,直到可以從某一個channel中讀取數據為止。
- select語句不會循環如果需要循環讀取,需要手動在select語句外加循環.
- 如下這個例子中,主線程使用select語句從c、c2任意一個channel中讀取數據。兩個協程分布向c,c2中發送數據,其中一個在1s后發送,另一個在2s后發送。可以看到主線程一開始無法從任何一個channel中讀取到數據,處於阻塞狀態。在1s時收到了c2的數據,然后就會繼續往下運行。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
c2 := make(chan int)
go func() {
time.Sleep(2 * time.Second)
c <- 1
}()
go func() {
time.Sleep(1 * time.Second)
c2 <- 2
}()
select {
case i := <-c:
fmt.Printf("receive from c: %d\n", i)
case i := <- c2:
fmt.Printf("receive from c2: %d\n", i)
}
}
- select語句還可以用於發送方。如下例子中,主線程將隨機挑選一個仍有緩沖區channel發送數據,如果緩沖區已滿,則這個channel的case語句將會被阻塞。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
c2 := make(chan int)
go func() {
fmt.Printf("receive from c: %d\n", <-c)
}()
go func() {
fmt.Printf("receive from c2: %d\n", <-c2)
}()
time.Sleep(time.Second)
select {
case c <- 1:
fmt.Printf("send c\n")
case c2 <- 1:
fmt.Printf("send c2\n")
}
}
- 注意close 一個channel也可以使select語句不再阻塞
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
time.Sleep(2 * time.Second)
close(c)
}()
select {
case i := <-c:
fmt.Printf("receive from c: %d\n", i)
}
}
輸出:
select配合default使用
- 使用default關鍵字。使用select語句時,我們可以使用default關鍵字。和switch類似,這是一個默認的分支。如果所有的channel都沒有准備好(例如對於發送者所有的channel都緩存已滿,或對於接受者所有channel的緩存已空),則程序會進入default分支的邏輯。
- 這是剛剛的select例子,唯一不同的是我們在select語句中加入了一個default分支。運行后可以發現,主線程沒有等待任何一個協程發送數據,而是直接進入了default的邏輯
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
c2 := make(chan int)
go func() {
time.Sleep(2 * time.Second)
c <- 1
}()
go func() {
time.Sleep(1 * time.Second)
c2 <- 2
}()
select {
case i := <-c:
fmt.Printf("receive from c: %d\n", i)
case i := <- c2:
fmt.Printf("receive from c2: %d\n", i)
default:
fmt.Println("default")
}
}
輸出:
使用time標准庫中的channel
- golang的time標准庫里提供了一些定時發送數據的channel,可以幫助我們實現一些功能。
- 例如利用
time.After()
函數配合select語句使用,可以實現超時的功能。本質上是time.After()
函數返回了一個channel並在我們設定的時間后向其發送一個數據。
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int)
go func() {
time.Sleep(2 * time.Second)
c <- 1
}()
select {
case i := <-c:
fmt.Printf("receive from c: %d\n", i)
case <-time.After(time.Second):
fmt.Println("timeout! ")
}
}
輸出:
- time標准庫中的
time.NewTicker()
函數返回一個帶有channel的結構體,並定時向這個結構體中發送時間數據,以實現定時器的功能。
package main
import (
"fmt"
"time"
)
func main() {
c := time.NewTicker(time.Second)
for t := range c.C {
fmt.Printf("receive t :%s\n", t)
}
}
輸出:
總結
- 通道(channel)則是用來傳遞數據的一個數據結構,除了傳遞基本類型的數據外還可以傳遞自定義的類型
- channel有帶緩沖區和不帶緩沖區(相當於緩沖區容量為0)兩種類型。當緩沖區已滿時會阻塞發送者,當緩沖區已空時會阻塞接受者。channel是線程安全的。
- 使用close關鍵字可以關閉channel。向已關閉的channel的發數據會panic。已關閉的channel中仍然可以讀取數據,可以通過接受ok參數判斷是否是從一個已關閉的channel中讀取的數據。
- 使用for range語法可以從channel中循環讀取數據,若channel為空,則循環會被阻塞,關閉channel會跳出循環。
- select case語句挑選一個能讀取出數據(或發送數據)的channel繼續執行。如果有多個channel滿足條件,則挑選其中任意一個。如果所有的channel都被阻塞,則select語句會被阻塞。使用default關鍵字可以避免select語句被阻塞。關閉一個channel同樣可以使select語句不再阻塞。
- time標准庫中提供了
time.After()
方法,返回一個定時發送數據的channel,可以和select語句配合實現超時的邏輯。time標准庫中還提供了time.NewTicker()
方法,返回一個帶有channel的結構體,並定時向這個結構體中發送時間數據,可以實現定時器的功能。