WHY?
Go的并发原语可以轻松构建流数据流水线,从而有效利用I/O和多个CPU。
WHAT?
管道是一种数据结构,发送方可以以字符流形式将数据送入该结构,接收方可以从该结构接收数据。
HOW?
Go中没有正式的管道定义;但它是众多并发程序中的一种,是通过通道(channel)连接的一系列阶段,且每个阶段是一组运行同一个函数的goroutine。在每个阶段:goroutine们
1.通过输入通道,从上游的获取数据
2.在数据上执行一系列数据,通常会产生新值
3.通过输出通道,将新产生的值送往下游
每个阶段都拥有任意数量的输入通道和输出通道,除了第一个和最后一个阶段,第一个阶段只有输出通道,最后一个阶段只有输入通道,第一个又被称为来源或生产者,最后一个阶段又被称为槽或消费者。
DO:
在正式代码之前,可以先从HOW之中的叙述大致推理出函数的样子。假设共有三个阶段,则三个阶段的函数形式类似:
func stage1() <-chan {}
func stage2(in <-chan) <-chan{}
func stage(in <-chan){}
这样心里就有数了。如果要求某些数的平方值:该如何运用Go管道的技术呢?
依上述伪代码尝试写一下。
首先第一阶段,要有数字,然后要把这些数以管道的形输出给下一阶段使用:
func genx(nums...int) <-chan int {
out := make(chan int)
for n := range nums {
out <- n
}
close(out)
return out
}
观察这个函数,符不符合上面的WHAT和HOW?
发现并不符合,HOW中要求每一个阶段都是一组goroutine的组合,而这个第一阶段并没有goroutine。所以要用到Goroutine而要用到goroutine且goroutine要满足从通过输入通道从上游获取值,通过输出通道向下游发送值,而且要处理旧值得到新值,意思是所有一切都要在Goroutine中处理,不能直接在函数中处理,要在函数中重开goroutine处理。
改动第一个阶段代码:
func genx(t *testing.T, nums...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
t.Logf("genx Closed")
close(out)
}()
return out
}
为什么把把close(out)放在goroutine中执行呢?因为向一个关闭的channel发送数据会引起panic,而goroutine的执行时必 genx的执行顺序慢的,所以如果放在外面,在把nums写入out的时候就会报错。
继续第二阶段,生成平方数:
func sqx(t *testing.T, in <-chan int) <-chan int{
out := make(chan int)
go func() {
for n := range in {
//t.Logf("NN=%v", n*n)
out <- n * n
}
t.Logf("sqx Closed")
close(out)
}()
return out
}
第三阶段,消费者输出:
func TestX(t *testing.T) {
a := genx(4, 5, 6)
b := sqx(t, a)
go func() {
for n := range b {
t.Logf("^o^_%v", n)
}
}()
}
这样对吗?运行一下发现,没有任何打印。这是为何?因为TestX运行在主goroutine中,一旦运行完毕程序立刻就退出了,并没有给TestX中的goroutine执行的机会。而我们在TestX的goroutine中调用的是range而不是 <- b这样的函数,不会引起函数的goroutine阻塞,直到有数据读入才停止阻塞。引起阻塞就不会退出主routine,就能看到打印。
所以第三阶段把代码修改为:
func TestX(t *testing.T) {
a := genx(4, 5, 6)
b := sqx(t, a)
for n := range b {
t.Logf("^o^_%v", n)
}
}
这下可以了,打印出16, 25,36。
这样貌似结束了。
但是还有一些问题。
如果输入的Goroutine不止一个呢?
这里牵涉到两个概念。扇入和扇出。
扇出:多个功能可以从同一个Channel读取,直到该通道关闭为止,提供了一种在一群工作者之间,分配工作给并行化CPU使用和IO的方法。
扇入:从多个输入读取,并继续执行,直到一切都停止下来。当以下情况发生时。通过复用多个输入的Channel到一个单独的Channel。当所有的输入都关闭的时候,这个Channel将会关闭(这个Channel关闭会导致一切【接收输入,输出等】都停止下来。)
扇出的代码:
func TestX1(t *testing.T) {
in := genx(4, 5, 6)
// 一个输入点(扇形的顶点),有多引用(扇形的扇面),扇出
c1 := sqx(t, in)
c2 := sqx(t, in)
for n := range mergex(c1, c2) {
t.Logf("n=%v", n)
}
}
扇入的代码:
// 扇入,很多输入(扇面)