channel
在之前第六、七章,介绍了go语言的协程并发机制,以及主程与协程采用sync包进行阻塞等待的工作方案。这次看看go语言官方推荐的协程并发解决方案–channel。
channel解决两个问题:
一是协程间的数据交互(在六、七节里面没有涉及)
二是协程间的阻塞方式
channel说明
创建一个channel用关键字make,指定类型为chan,关闭一个channel用close
保持良好习惯,使用完的channel就关闭,在数据输入侧(生产侧)进行关闭操作。
ch1 := make(chan int)
ch2 := make(chan int,10)
如上,我们创建一个无缓冲的管道ch1,以及一个允许缓冲10个int 数据的管道ch2。
固定用法:channel要求指明传输的数据类型,比如int,string等。
管道里传输和读取数据使用箭头 <- 符号,数据流入和流出看箭头指向。
//向管道传数据
var a:=3
ch1<-a
//从管道取数据
var b int
b = <-ch1
继续下面的代码:
package main
import (
"fmt"
"time"
)
func worker1(v chan int) {
defer close(v)
for i := 0; i < 5; i++ {
fmt.Println("input i=", i)
v <- i
}
}
func main() {
ch1 := make(chan int)
go worker1(ch1)
for {
time.Sleep(time.Second * 1)
a, b := <-ch1
fmt.Println("output a=", a)
fmt.Println("output b=", b)
}
}
我们先定义一个worker1,他接收一个channel,负责往这个channel里写数据,因为worker1算是生产者(产生数据端),所以他负责关闭channel,这里使用的是defer(close)方法。
在主程中,我们开启一个协程跑worker1,主程则循环从channel里取数据。主程与协程通过管道ch1来交流数据。执行结果如下。
这里a,b := <- ch1,是从ch1里取值,a代表真实的数据,b代表channel的状态。
当channel状态正常时,b 为 true,当channel被关闭时,b为false,但a此时读取的是管道指定传输类型的零值。
所以可以使用b进行逻辑判断,是否退出。另一种更为常用的写法,是使用range。
package main
import (
"fmt"
)
func worker1(v chan int) {
defer close(v)
for i := 0; i < 5; i++ {
fmt.Println("input i=", i)
v <- i
}
}
func main() {
ch1 := make(chan int)
go worker1(ch1)
for a := range ch1 {
fmt.Println("output a=", a)
}
}
如上,range 后面直接写管道名,会默认读取管道数据,检测到管道关闭,range自身就会退出。
我们再看看channel的阻塞特点,如上代码,让worker1一直循环不再退出,主程将一直处于等待态
package main
import (
"fmt"
)
func worker1(v chan int) {
defer close(v)
for i := 0; i < 5; i++ {
fmt.Println("input i=", i)
v <- i
}
for {
}
}
func main() {
ch1 := make(chan int)
go worker1(ch1)
for a := range ch1 {
fmt.Println("output a=", a)
}
}
因为worker1后面有空循环,所以永远执行不到close(ch1),这时虽然管道ch1内没有任何数据,但管道本身有效,并未关闭,所以range不会退出,故主程会以阻塞态等待ch1后续有数据产生或者直到ch1关闭为止。
其他
有些地方把go里采用有缓冲的管道称为非阻塞模式或者异步模式,采用了无缓冲的管道成为阻塞或者同步模式,其实缓冲只是给你的程序设计有个缓冲数据的空间,本质上仍然是阻塞的机制。只不过有缓冲区的时候,一开始并不会触发阻塞条件。如果及时处理缓冲内的数据,或者缓冲区给的足够大,程序跑起来,就像是一个非阻塞或者异步的表象。