go中关于chan应用的程序分析

相关文章:

  • chan基本知识
  • go中关于for循环的知识
  • 多进程通信
前言

学习go语言已经有几个月,但是关于go中的特性chan和routine的应用还不是很理解,如果搞不懂chan和routine的机制就很难流畅的用go编写出健壮的程序。所以我觉得关于chan应用的编程,是可以讲一讲的。
那我们首先会想到几个问题:

  • 使用chan的代码与普通的代码有什么不同吗?
  • 使用了chan后有什么好处吗?
  • 怎么才能正确的使用chan呢?

我们先看第一个问题,众所周知 在go中chan是用来多个协程之间进行通信的,chan的应用思维是一种类似与client/servier的思维。也就是要有信息的生产者和消费者。既然如此,那就会设计到 不通协程之间的通信,而在顺序编程中是不会设计到通信的,是一种线性的流程。类似与下图:
在这里插入图片描述
在图中,我们看到,生产者和消费者之间的程序通过通信来进行相互影响的,而线性的程序是不会有这种问题,不管调用了多少函数,都只会在一条进程上顺序进行。
在下文中我们会用到一个中间者的概念。中间者是承接上游的消费者和开启下游的生产者

单中间者

单中间者,就是在程序中只有一个线程连接生产者和消费者,不会进行扩增。

如下图所示:
在这里插入图片描述
在这种方式中,只有一个中间者,或者没有中间者。

  1. 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中。

  1. 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中。

  1. 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的中间者收集各个中间者生产的信息,并将这些信息统一的交给消费者。

  1. 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(也就是消费者),而通道内的数据不进行消费就会形成阻塞,导致程序无法运行。
    *那我已经知道了不能用在函数体里面,为什么我另起一条协程,不会出问题呢?? 这涉及到后面部分,如果你明白了程序运行的整个流程,肯定可以解决这个疑问,我们把这个问题留到后面解决
  1. 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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值