相关文章:
- chan基本知识
- go中关于for循环的知识
- 多进程通信
前言
学习go语言已经有几个月,但是关于go中的特性chan和routine的应用还不是很理解,如果搞不懂chan和routine的机制就很难流畅的用go编写出健壮的程序。所以我觉得关于chan应用的编程,是可以讲一讲的。
那我们首先会想到几个问题:
- 使用chan的代码与普通的代码有什么不同吗?
- 使用了chan后有什么好处吗?
- 怎么才能正确的使用chan呢?
我们先看第一个问题,众所周知 在go中chan是用来多个协程之间进行通信的,chan的应用思维是一种类似与client/servier的思维。也就是要有信息的生产者和消费者。既然如此,那就会设计到 不通协程之间的通信,而在顺序编程中是不会设计到通信的,是一种线性的流程。类似与下图:
在图中,我们看到,生产者和消费者之间的程序通过通信来进行相互影响的,而线性的程序是不会有这种问题,不管调用了多少函数,都只会在一条进程上顺序进行。
在下文中我们会用到一个中间者的概念。中间者是承接上游的消费者和开启下游的生产者。
单中间者
单中间者,就是在程序中只有一个线程连接生产者和消费者,不会进行扩增。
如下图所示:
在这种方式中,只有一个中间者,或者没有中间者。
- gen.go
func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
分析:创建一个out的通道,并打开一条协程将nums通过循环传递给通道out,在所有的nums都传入out后关闭通道。在打开协程后返回创建的out。
这个函数在程序中起着生产者的作用,生产者和消费这之间的交流通道就是这个out,当out中消费一个数后,本函数就会再将一个数推送到通道out中。
- sq.go
func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
创建了通道out作为生产者的信息输出,并接受in作为消费者的信息输入。
这程序中,即是消费者也是生产者,并在中间进行了数据的处理nn。作为消费者,当通道in中可以取数据的时候,取出n并进行运算nn,然后作为生产者将结果推送到out中。当in中有值时会进行消费,并计算nn,当out为空时会进行生产,将nn的结果推到out中。
- main.go
func main() {
in := gen(2, 3)
c1 := sq(in)
for n := range c1 {
fmt.Println(n) // 4 9
}
}
main函数先调用了上面两个函数,然后用了一个循环进行输出。
输出这个循环就是整个程序的消费者,当c1中有值时,就会取出来放到n中。
单中间者并不能体现使用chan的优越性,整个过程仍然是类似于线性的流程进行。在下文中我们会开启多个中间者进行复杂的数据运算和处理,因为我们可以同时处理多条数据,必然给我们带来性能上的提高。
多中间者
单中间者,就是在程序中只有一个线程连接生产者和消费者,不会进行扩增。
在上图中,一个生产者被多个中间者处理生产的信息,以提高消费的效率。然后用一个merge的中间者收集各个中间者生产的信息,并将这些信息统一的交给消费者。
- merge.go
func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)
// Start an output goroutine for each input channel in cs. output
// copies values from c to out until c is closed, then calls wg.Done.
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}
// Start a goroutine to close out once all the output goroutines are
// done. This must start after the wg.Add call.
go func() {
time.Sleep(222 * time.Second)
wg.Wait()
close(out)
}()
return out
}
这个merge函数给我带来了很大的困扰。
- 程序中有两个for循环,其中对cs的循环是针对数组的,循环的作用是对每个通道打开一个协程。而协程中的循环是监控通道用的,当通道close时推出循环。
- 为什么wg.Wait要用一个新的协程来,如果不用新的协程而写在函数体里面不行吗?当然是不行的,这里要注意一个与普通程序的分别:在使用chan的程序中,主程序的作用类似与初始化service和client而不进行具体的运算,所以肯定不能在初始化的时候进行wait,因为这里还是要进行生产,如果进行wait就不会运行main中的print(也就是消费者),而通道内的数据不进行消费就会形成阻塞,导致程序无法运行。
*那我已经知道了不能用在函数体里面,为什么我另起一条协程,不会出问题呢?? 这涉及到后面部分,如果你明白了程序运行的整个流程,肯定可以解决这个疑问,我们把这个问题留到后面解决
- main.go
func main() {
in := gen(2, 3)
// Distribute the sq work across three goroutines that both read from in.
c1 := sq(in)
c2 := sq(in)
// Consume the merged output from c1 and c2.
for n := range merge(c1, c2) {
fmt.Println(n) // 4 then 9, or 9 then 4
}
}
在函数中先用生产者,通过通道传递两个数,然后用两个sq中间者进行中间计算,然后用merge进行集合,最后通过print进行消费。
在初次看到这个程序的时候,我是觉得程序不会输出所有的结果的看,为什么我会这么看呢?因为在merge中的wait是在另起的一个协程中运行的,而不是在主程序中。如果不能理清楚整个程序的流程就会第一时间出现这种误解。那我们只有模拟程序一步一步的运行,来判断为什么这个程序可以顺利的运行。
流程图
因此我画了下面的流程图,图中标注了程序的step。(不通协程之间的step是可以重合的)
我们来过一遍上图的整个流程,你就发现程序中的所有疑问都迎刃而解。(虽然我们这里使用了多个中间者的方式,但是因为我们仅仅模拟了一个数据输入,所以只展示一个中间者就可以表示整个流程)
流程图分析
step 0:
[main] 调用程序 gen(3)
step 1:
[gen] 定义通道 out
step 2:
step 3:
[gen] 进入协程sub(gen)中的循环,判断循环是否结束,同时return out
step 4:
[main] 进入程序sq
[sq] 定义通道out,gen中的out定义为in
[sub(gen)] n-> out ##因为还没有遇到消费者,所以造成了阻塞
step 5:
[sq]进入协程sub(sq)中的循环,判断循环是否结束,同时r