学习GO语言的Fan In 和Fan Out模式

689 阅读3分钟

欢迎来到涵盖Go中并发模式系列的文章,这次我谈论的是两个类似的模式Fan InFan Out


什么是扇入模式?

Fan-In 模式包括通过复用每个接收的值将多个通道合并成一个通道,它的工作方式是将每个接收通道的消息合并成一个。

What is Concurrency?

比如说

func main() {
	ch1, err := read("file1.csv")
	if err != nil {
		panic(fmt.Errorf("Could not read file1 %v", err))
	}

	ch2, err := read("file2.csv")
	if err != nil {
		panic(fmt.Errorf("Could not read file2 %v", err))
	}

	//-

	exit := make(chan struct{})

	chM := merge2(ch1, ch2)

	go func() {
		for v := range chM {
			fmt.Println(v)
		}

		close(exit)
	}()

	<-exit

	fmt.Println("All completed, exiting")
}

这个main 函数可能看起来很多,但让我们把它拆开。

  1. 我们有一个read 函数,接收一个CSV文件名并返回一个接收通道。
  2. 使用该read 函数读取两个文件:file1.csvfile2.csv
  3. 我们用这些通道作为merge2 函数的参数,然后返回另一个通道。
  4. 该通道的接收值在一个goroutine中被打印出来。
  5. 有一个通道exit ,只用于确保匿名goroutine完成读取所有的值。
  6. 这个通道exit 被关闭,并触发一个优雅的关闭

Fan-In 模式的实现是在 merge2:

func merge2(cs ...<-chan []string) <-chan []string {
	chans := len(cs)
	wait := make(chan struct{}, chans)

	out := make(chan []string)

	send := func(c <-chan []string) {
		defer func() { wait <- struct{}{} }()

		for n := range c {
			out <- n
		}
	}

	for _, c := range cs {
		go send(c)
	}

	go func() {
		for range wait {
			chans--
			if chans == 0 {
				break
			}
		}

		close(out)
	}()

	return out
}

这就是很酷的事情发生的地方,对于每一个收到的通道值,我们使用匿名的send 函数启动一个goroutine,所有这些都将消息发送到out 通道,该通道被返回到我们的初始调用,然后在上述通道关闭后关闭。

注意我们还在等待一个wait 通道,用于确定何时关闭out 通道,这个通道是为了接收由send 中的defer 触发的N条消息;这被用作指示通道为空,并且goroutine完成了它的工作。

另一种做类似事情的方法是使用sync.WaitGroup 类型,例如另一种做类似事情的方法是像在 merge1做类似的事情。

func merge1(cs ...<-chan []string) <-chan []string {
	var wg sync.WaitGroup

	out := make(chan []string)

	send := func(c <-chan []string) {
		for n := range c {
			out <- n
		}

		wg.Done()
	}

	wg.Add(len(cs))

	for _, c := range cs {
		go send(c)
	}

	go func() {
		wg.Wait()

		close(out)
	}()

	return out
}

什么是 "扇出 "模式?

Fan-Out 模式包括通过分配每个值将一个通道分解成多个通道,它的工作方式是通过返回接收来自原始通道的值的通道。

What is Concurrency?

比如说

func main() {
	ch1, err := read("file1.csv")
	if err != nil {
		panic(fmt.Errorf("Could not read file1 %v", err))
	}

	//-

	br1 := breakup("1", ch1)
	br2 := breakup("2", ch1)

	for {
		if br1 == nil && br2 == nil {
			break
		}

		select {
		case _, ok := <-br1:
			if !ok {
				br1 = nil
			}
		case _, ok := <-br2:
			if !ok {
				br2 = nil
			}
		}
	}

	fmt.Println("All completed, exiting")
}

这个main ,有点类似于我们在Fan-Out中的做法。

  1. 我们有一个read 函数,接收一个CSV文件名并返回一个接收通道。
  2. 这个返回的通道,ch1 ,是一个要被分解的通道。
  3. 为了做到这一点,我们使用breakup ,返回另一个通道。
  4. 最后,使用一个无限的foor 循环,等待我们的通道被关闭后退出。

breakup 这个程序是这样实现的。

func breakup(worker string, ch <-chan []string) chan struct{} {
	chE := make(chan struct{})

	go func() {
		for v := range ch {
			fmt.Println(worker, v)
		}

		close(chE)
	}()

	return chE
}

如果你注意到它只是把接收到的值打印出来,我们可以做一些其他的事情,比如增强或过滤这些值;我们也可以重写这个例子,在返回的通道中实际发送这些值,这样该通道的用户可以对接收到的值做一些事情。

结论

Fan-In 和 是Fan-Out Go中最常见的两种并发模式,根据你的问题,你可能会发现它们很有用,重要的是要始终牢记一种方法来确定通道何时关闭,并适当地在下游发出一个消息来表明这一点,以便他们能够做出正确的反应。