Golang 并發(fā) Channel的用法
channel 的創(chuàng)建
ch := make(chan int)
上面是創(chuàng)建了無緩沖的 channel,一旦有 goroutine 往 channel 發(fā)送數(shù)據(jù),那么當前的 goroutine 會被阻塞住,直到有其他的 goroutine 消費了 channel 里的數(shù)據(jù),才能繼續(xù)運行。
ch := make(chan int, 2)
上面示例中的第二個參數(shù)表示 channel 可緩沖數(shù)據(jù)的容量。只要當前 channel 里的元素總數(shù)不大于這個可緩沖容量,則當前的 goroutine 就不會被阻塞住。
nil channel
nil是pointers, interfaces, maps, slices, channels 和 function 類型的零值,表示未初始化值。nil不是未定義狀態(tài),它本身就是值。error是接口類型,因此error變量可以為nil,但string不能為nil。
下面我們看下nil 通道有什么特點,空通道對操作的反應如下:
- 從空通道讀、寫會永遠阻塞
- 關閉通道會終止程序(panic)
空通道是一種特殊通道,總是阻塞。對比非空已關閉的通道仍然可以進行讀取,并能夠讀取對應類型的零值,但對于已關閉的通道發(fā)送信息會終止程序。
一般 nil channel 用在 select 上,讓 select 不再從這個 channel 里讀取數(shù)據(jù)
讀寫阻塞示例
示例如下:
func TestNil(t *testing.T) {
c := make(chan int)
go sendIntegers(c)
addIntegers(c)
}
func addIntegers(c chan int) {
sum := 0
t := time.NewTimer(time.Second * 5)
for {
select {
case input := <-c:
sum = sum + input
fmt.Println("addIntegers , input : " + strconv.Itoa(input) + " , sum : " + strconv.Itoa(sum))
case <-t.C:
c = nil
fmt.Println("addIntegers , nil channel , sum : " + strconv.Itoa(sum))
}
}
}
func sendIntegers(c chan int) {
for {
time.Sleep(time.Second * 1)
c <- rand.Intn(100)
}
}
輸出如下
=== RUN TestNil
addIntegers , input : 81 , sum : 81
addIntegers , input : 87 , sum : 168
addIntegers , input : 47 , sum : 215
addIntegers , input : 59 , sum : 274
addIntegers , nil channel , sum : 274
panic: test timed out after 30s
此示例會一直阻塞下去,addIntegers是程序的主協(xié)程會一直阻塞下去,sendIntegers是子協(xié)程同樣會一直阻塞下去。
其中:輸出中的panic是單元測試的Test引發(fā)的異常,不需要考慮在內(nèi)。
close示例
func TestCloseNil(t *testing.T) {
c := make(chan int)
go writeChannel(c)
num := <-c
fmt.Println("main goroutine , read num : " + strconv.Itoa(num))
c = nil
fmt.Println("main goroutine , to close channel .")
close(c)
time.Sleep(time.Second * 10)
}
func writeChannel(c chan int) {
fmt.Println("writeChannel goroutine , running ...")
c <- 1
}
輸出如下
=== RUN TestCloseNil
writeChannel goroutine , running ...
main goroutine , read num : 1
main goroutine , to close channel .
--- FAIL: TestCloseNil (0.00s)
panic: close of nil channel [recovered]
panic: close of nil channel
關閉nil通道會引起程序panic
channel 的讀寫
寫操作
ch := make(chan int)
ch <- 1
讀操作
data <- ch
當我們不再使用 channel 的時候,可以對其進行關閉:
close(ch)
不過讀取關閉后的 channel,不會產(chǎn)生 pannic,還是可以讀到數(shù)據(jù)。
如果關閉后的 channel 沒有數(shù)據(jù)可讀取時,將得到零值,即對應類型的默認值。
為了能知道當前 channel 是否被關閉,可以使用下面的寫法來判斷。
if v, ok := <-ch; !ok {
fmt.Println("channel 已關閉,讀取不到數(shù)據(jù)")
}
還可以使用下面的寫法不斷的獲取 channel 里的數(shù)據(jù):
for data := range ch {
// get data dosomething
}
這種用法會在讀取完 channel 里的數(shù)據(jù)后就結束 for 循環(huán),執(zhí)行后面的代碼。
channel 只讀只寫
在默認情況下,管道是雙向的,可讀可寫,在使用 channel 時我們還可以控制 channel 只讀只寫操作:
聲明為只寫,如下:
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2 <- 20
如果試著讀此chan,則編譯報錯,編譯錯誤如下:
invalid operation: cannot receive from send-only channel chan2 (variable of type chan<- int) compiler (InvalidReceive)
聲明為只讀,不可寫,否則編譯報錯,如下:
var chan3 <-chan int
nm2 := <-chan3
函數(shù)可以聲明chan只讀只寫,代碼示例:
// 只寫操作
func send(ch chan<- int, exitChan chan struct{}) {
for i := 0; i < 5; i++ {
time.Sleep(time.Second * 1)
ch <- i
}
close(ch)
var a struct{}
exitChan <- a
}
// 只讀操作
func recv(ch <-chan int, exitChan chan struct{}) {
for {
v, ok := <-ch
if !ok {
break
}
fmt.Println("recv goroutine , value : " + strconv.Itoa(v))
}
var a struct{}
exitChan <- a
}
func TestOnlyReadWrite(t *testing.T) {
ch := make(chan int, 10)
exitChan := make(chan struct{}, 2)
go send(ch, exitChan)
go recv(ch, exitChan)
var total = 0
for _ = range exitChan {
total++
if total == 2 {
break
}
}
fmt.Println("main goroutine , 結束")
}
輸出如下:
=== RUN TestOnlyReadWrite
recv goroutine , value : 0
recv goroutine , value : 1
recv goroutine , value : 2
recv goroutine , value : 3
recv goroutine , value : 4
main goroutine , 結束
--- PASS: TestOnlyReadWrite (5.03s)
關閉channel
channel關閉后,剩余的數(shù)據(jù)能否取到
golang channel關閉后,其中剩余的數(shù)據(jù),是可以繼續(xù)讀取的,channel關閉之后,仍然可以從channel中讀取剩余的數(shù)據(jù),直到數(shù)據(jù)全部讀取完成。
對于關閉的channel的讀寫需要注意兩點:
- 如果繼續(xù)向channel發(fā)送數(shù)據(jù),會引起panic,
- 如果繼續(xù)讀數(shù)據(jù),得到的是零值(對于int,就是0)。
讀取關閉的channel,將獲取零值
當讀取已關閉的channel時,如果繼續(xù)讀取channel,獲取到的是零值,不會堵塞,
另外即使是無緩沖的channel,也將能一直獲取到零值。
代碼示例如下
func TestCloseDemo01(t *testing.T) {
done := make(chan struct{})
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
go func() {
for {
value := <-ch
//此處為假設判斷,value永遠不會等于10
if value == 10 {
break
}
fmt.Println("read channel , value : ", value)
time.Sleep(time.Second * 1)
}
done <- struct{}{}
}()
select {
case <-done:
fmt.Println("讀取channel,正常結束")
case <-time.After(time.Second * 5):
fmt.Println("超時退出")
}
}
輸出如下:
=== RUN TestCloseDemo01
read channel , value : 1
read channel , value : 2
read channel , value : 3
read channel , value : 0
read channel , value : 0
超時退出
--- PASS: TestCloseDemo01 (5.00s)
使用ok判斷,是否關閉
讀取channel,判斷是否關閉:
value, ok := <-ch
- 當channel關閉時,ok=false
- 當channel未關閉時,ok=true
通過判斷channel是否關閉,當channel關閉時,程序可以正常退出,代碼示例如下:
func TestCloseDemo02(t *testing.T) {
done := make(chan struct{})
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
go func() {
for {
value, ok := <-ch
if !ok {
break
}
fmt.Println("read channel , value : ", value)
time.Sleep(time.Second * 1)
}
done <- struct{}{}
}()
select {
case <-done:
fmt.Println("讀取channel,正常結束")
case <-time.After(time.Second * 5):
fmt.Println("超時退出")
}
}
輸出如下:
=== RUN TestCloseDemo02
read channel , value : 1
read channel , value : 2
read channel , value : 3
讀取channel,正常結束
--- PASS: TestCloseDemo02 (3.03s)
PASS
使用for-range退出
for-range是使用頻率很高的結構,常用它來遍歷數(shù)據(jù),range能夠感知channel的關閉,當channel被發(fā)送數(shù)據(jù)的協(xié)程關閉時,range就會結束,接著退出for循環(huán)。
它在并發(fā)中的使用場景是:當協(xié)程只從1個channel讀取數(shù)據(jù),然后進行處理,處理后協(xié)程退出。
下面這個示例程序,當通道被關閉時,協(xié)程可自動退出。
func TestCloseDemo02(t *testing.T) {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch)
for v := range ch {
fmt.Println("value", v)
}
time.Sleep(time.Second * 10)
}
使用close(ch)關閉所有下游協(xié)程
- 關閉通道,可以主動通知所有協(xié)程退出的場景
當啟動100個worker時,只要main()執(zhí)行關閉stopCh,每一個worker都會都到信號,進而關閉。如果main()向stopCh發(fā)送100個數(shù)據(jù),這種就低效了。
//close關閉所有子協(xié)程
func TestCloseDemo04(t *testing.T) {
ch := make(chan int, 3)
stopCh := make(chan struct{})
for i := 1; i < 6; i++ {
worker("worker"+strconv.Itoa(i), stopCh, ch)
}
time.Sleep(time.Second * 5)
close(stopCh)
time.Sleep(time.Second * 5)
}
func worker(workerName string, stopCh <-chan struct{}, ch <-chan int) {
go func() {
defer fmt.Println(workerName, "goroutine , worker exit")
// Using stop channel explicit exit
for {
select {
case <-stopCh:
fmt.Println(workerName, "goroutine , Recv stop signal , return")
return
default:
fmt.Println(workerName, "goroutine , worker default ...")
}
time.Sleep(time.Second * 3)
}
}()
}
輸出如下
=== RUN TestCloseDemo04
worker5 goroutine , worker default ...
worker3 goroutine , worker default ...
worker4 goroutine , worker default ...
worker1 goroutine , worker default ...
worker2 goroutine , worker default ...
worker3 goroutine , worker default ...
worker2 goroutine , worker default ...
worker5 goroutine , worker default ...
worker4 goroutine , worker default ...
worker1 goroutine , worker default ...
worker4 goroutine , Recv stop signal , return
worker4 goroutine , worker exit
worker2 goroutine , Recv stop signal , return
worker2 goroutine , worker exit
worker5 goroutine , Recv stop signal , return
worker5 goroutine , worker exit
worker1 goroutine , Recv stop signal , return
worker1 goroutine , worker exit
worker3 goroutine , Recv stop signal , return
worker3 goroutine , worker exit
--- PASS: TestCloseDemo04 (10.01s)
PASS
函數(shù)傳遞引用or值
golang 傳遞給函數(shù)chan類型時,是值傳遞和引用傳遞?
- golang默認都是采用值傳遞,即拷貝傳遞
- 有些值天生就是指針(slice、map、channel)
可以看出來map和slice都是指針傳遞,即函數(shù)內(nèi)部是可以改變參數(shù)的值的。而array是數(shù)組傳遞,不管函數(shù)內(nèi)部如何改變參數(shù),都是改變的拷貝值,并未對原值進行處理。
在 Go 語言中,所有的函數(shù)參數(shù)傳遞都是值傳遞(pass by value),當將參數(shù)傳遞給函數(shù)時,實際上是將參數(shù)的副本傳遞給函數(shù)。然而,這并不意味著在函數(shù)內(nèi)部對參數(shù)的修改都不會影響原始數(shù)據(jù)。因為在 Go 中,有些數(shù)據(jù)類型本身就是引用類型,比如切片(slice)、映射(map)、通道(channel)、接口(interface)和指針(pointer)。當這些類型作為參數(shù)傳遞給函數(shù)時,雖然傳遞的是值,但值本身就是一個引用。
小結
Go 語言中的參數(shù)傳遞總是值傳遞,意味著傳遞的總是變量的副本,無論是基本數(shù)據(jù)類型還是復合數(shù)據(jù)類型。由于復合數(shù)據(jù)類型(如切片、映射、通道、接口和指針)內(nèi)部包含的是對數(shù)據(jù)的引用,所以在函數(shù)內(nèi)部對這些參數(shù)的修改可能會影響到原始數(shù)據(jù)。理解這一點對于編寫正確和高效的Go代碼至關重要。文章來源:http://www.zghlxwxcb.cn/news/detail-832661.html
另外即使是引用類型,比如切片,當長度或容量(比如使用 append 函數(shù))發(fā)生變化了,可能會導致分配新的底層數(shù)組。這種情況下,原始切片不會指向新的數(shù)組,但是函數(shù)內(nèi)部的切片會。因此,如果想在函數(shù)內(nèi)部修改切片的長度或容量并反映到外部,應該傳遞一個指向切片的指針。文章來源地址http://www.zghlxwxcb.cn/news/detail-832661.html
參考
- https://www.cnblogs.com/-wenli/p/12350181.html
- https://segmentfault.com/a/1190000017958702
- https://zhuanlan.zhihu.com/p/395278270
- https://zhuanlan.zhihu.com/p/613771870
- Go里面如何實現(xiàn)廣播 https://juejin.cn/post/6844903857395335182
到了這里,關于Golang 并發(fā) Channel的用法的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!