耶,第十三天,昨天講了goroutine
,今天就來講channel
吧!你說這兩個有什麼關聯?我一開始也想了很久,希望今天的文章可以解釋到
queue
<-
將資料放入與取出channel
內沒有東西,程式會等待另一端操作完ch := make(chan int , n) // 建立 int 型別長度為 n 的 Channel
使用 <-
來操作資料,想像channel
為管道,右進左出,ch<-data
為將data
放入,<-ch
為取出資料
ch := make(chan int , 1)
ch <- 5
fmt.Println(<-ch) // 5
可利用len()
與cap()
來確認裡面有多少筆資料與容量,
ch := make(chan int , 1) // (len(ch),cap(ch)) = 0,1
ch <- 5 // (len(ch),cap(ch)) = 1,1
fmt.Println(<-ch) // 5 // (len(ch),cap(ch)) = 0,1
可以看出5被放入時len=1
,取出後len=0
Channel
在推入與拉出資料時都有可能發生阻塞問題
channel
時,在這筆資料拉出之前,其他的ch<-data
會被卡住ch
內沒資料時,<-ch
會卡住,直到channel
內有資料時才執行推入資料的等待情境
func main() {
ch := make(chan int, 1)
ch <- 1
ch <- 2
fmt.Println("Hi")
}
卡在 ch <- 2
這行,程式直接暫停
拉出資料的等待情境
func main() {
ch := make(chan int)
fmt.Println(<-ch)
fmt.Println("Hi")
}
長度為0,資料放不進去,<-ch
這邊卡住,又暫停了
好啦,那這個像水管一樣的資料型態,什麼時候可以用上?從昨天的goroutine
與Race Condition
開始講好了。
競爭條件?這是什麼東西,我們先看一下下面的例子
func main() {
total := 0
for i := 0; i < 1000; i++ {
go func(){
total++
}()
}
time.Sleep(time.Second)
fmt.Println(total)
}
output:
982
輸出怎麼不是1000?其實也不一定是982,每次執行數字都會浮動。那是因為goroutine
在為total
進行++
時,說不定還沒處理完並存成新的total
,原先的goroutine
就又進入了迴圈並取得了total
去做運算,這樣雖然跑了兩次++
,但其實total
只加了一次,這是多執行續時一定會遇上的問題
帶數字舉例。total
此時是50
go func
去做total=total+1
main
又進了迴圈一次,跑go func
goroutine2
取得的total
還是50goroutine1
與goroutine2
同時運算完,兩個都回傳total=51
多執行續偶爾會遇到這種變數問題。這就是為什麼上面的答案會與預期有落差。
這在作業系統上稱做Race Condition
,有更簡單的例子啦,只是我想結合goroutine
一起講,可以查一下其他Race Condition
的例子。
有種做法是為變數上鎖,當有人在使用時就禁止別人使用,Go可以使用互斥鎖來實做這個方法。
type SafeNumber struct {
v int
mux sync.Mutex // 互斥鎖
}
func main() {
total := SafeNumber{v:0}
for i := 0; i < 1000; i++ {
go func(){
total.mux.Lock()
total.v++
total.mux.Unlock()
}()
}
time.Sleep(time.Second)
total.mux.Lock()
fmt.Println(total.v)
total.mux.Unlock()
}
output:
1000
SafeNumber
結構,內有sync.Mutex
互斥鎖SafeNumber.v
的前後都使用Lock
、Unlock
上鎖與開鎖,確保同一時間只有一執行續在操作變數。Go提供了channel
這個資料型態來完成操作共同變數的功能,Go官方文章說明了這點。
同個例子改成使用channel
func main() {
ch := make(chan int, 1)
ch <- 0
for i := 0; i < 1000; i++ {
go func() {
ch <- (<-ch + 1)
}()
}
time.Sleep(time.Second)
fmt.Println(<-ch)
}
output:
1000
每一個go func
執行順序如下
<-ch
拉出ch內的資料(<-ch + 1)
將拉出來的資料+1ch <- (<-ch + 1)
推回ch因此就算goroutine2
趕上了goroutine1
,只要goroutine1
還沒將資料放回ch
,goroutine2
就會進入等待期,避免搶奪問題發生
Channel
又分為這兩種。Buffered
,緩衝區,顧名思義就是有沒有緩衝區的channel
而已。在宣告時會一併宣告channel
大小,若沒輸入預設長度為0。
Unbuffered Channel
: 在使用上若有東西塞入channel
,一定要靠另一個goroutine
將東西拉出去後才有辦法繼續執行,否則會卡住Buffered Channel
: 可將東西儲存至容量上限,等待另一端將資料取出在goroutine
進入等待期時使用者是不清楚的,因此可以使用select
讓程式在進入等待期時也能有一點輸出。
ch := make(chan string)
go func() {
time.Sleep(time.Second) //模擬費時運算
ch <- "FINISH"
}()
for {
select {
case (<-ch): // Channel 中有資料執行此區域
fmt.Println("main完成")
return
default: // Channel 阻塞的話執行此區域
fmt.Println("waiting...")
time.Sleep(500 * time.Millisecond)
}
}
output:
waiting...
waiting...
waiting...
main完成
這種方式就能在等待期也輸出一點資料
完成啦,又完成了一篇耗時的文章,跟昨天一樣卡了很久看不懂到底要channel
幹嘛,明明原本的資料型態就夠了幹嘛還多學一個。不過感覺與goroutine
一樣以後在寫系統相關程式或微服務時應該都會用到,先學起來以後用吧
Go 的並發:Goroutine 與 Channel 介紹
https://peterhpchen.github.io/2020/03/08/goroutine-and-channel.html
olang 教學系列 - 何謂 Channel? 先從宣告 Channel 開始學起!
https://blog.kennycoder.io/2020/12/23/Golang%E6%95%99%E5%AD%B8%E7%B3%BB%E5%88%97-%E4%BD%95%E8%AC%82Channel-%E5%85%88%E5%BE%9E%E5%AE%A3%E5%91%8AChannel%E9%96%8B%E5%A7%8B%E5%AD%B8%E8%B5%B7/