goroutine和channel 如何控制并发顺序?

写在前面

最近有同学问我这个问题。

在这里插入图片描述

题目意思是 利用goroutine和channel 连续输出10次,dog,cat,fish,并且都要按照这个dog,cat,fish的顺序输出。

分析

题目既然要求是使用goroutine,那么我们肯定是要控制好这个并发的顺序。因为并发是具有随机性的,这个题目并不难,很典型的chan控制进程之间的顺序。

那我们先了解一下 goroutine ,select ,sync.WaitGroup,channel

1. goroutine

我们这里先了解一下go的调度机制,即是GPM模型。goruntine相对线程更加轻量,GPM调度器效率更高。

  • G:Goroutine 我们所说的协程,为用户级的轻量级线程,每个Goroutine对象中的sched保存着其上下文信息
  • P:Processor 调度,即为G和M的调度对象,用来调度G和M之间的关联关系,其数量可通过GOMAXPROCS()来设置,默认为核心数
  • M:Machine 真正的工人,对内核级线程的封装,数量对应真实的CPU数

每个Processor对象都拥有一个LRQ(Local Run Queue),未分配的Goroutine对象保存在GRQ(Global Run Queue )中,等待分配给某一个P的LRQ中,每个LRQ里面包含若干个用户创建的Goroutine对象。

同时Processor作为桥梁对Machine和Goroutine进行了解耦,也就是说Goroutine如果想要使用Machine需要绑定一个Processor才行。

2. select

selectswitch 很像,它不需要输入参数,并且仅仅被使用在通道操作上
select 语句被用来执行多个通道操作的一个和其附带的 case 块代码。

扫描二维码关注公众号,回复: 13714742 查看本文章

我们知道 select 语句和 switch 很像,不同点是用通道读写操作代替了布尔操作。
通道将被阻塞,除非它有默认的 default 块 (之后将介绍)。一旦某个 case 条件执行,它将不阻塞。
我们发现 select 语句将阻塞,因此 select 将等待,直到有 case 语句不阻塞。

可以使用 select 模拟了一个数百万请求的服务器负载均衡的例子,它从多个有效服务中返回其中一个响应。

使用协程,通道和 select 语句,我们可以向多个服务器请求数据并获取其中最快响应的那个。

3. sync.WaitGroup

WaitGroup 是一个带着计数器的结构体,这个计数器可以追踪到有多少协程创建,有多少协程完成了其工作。当计数器为 0 的时候说明所有协程都完成了其工作。

  • Add 方法的参数是一个变量名叫 delta 的int 类型参数,主要用来内部计数。 内部计数器默认值为 0. 它用于记录多少个协程在运行。

  • WaitGroup 创建后,计数器值为 0,我们可以通过给 Add方法传 int类型值来增加它的数量。 记住, 当协程建立后,计数器的值不会自动递增 ,因此需要我们手动递增它。

  • Wait 方法用来阻塞当前协程。一旦计数器为 0, 协程将恢复运行。 因此,我们需要一个方法去降低计数器的值。

  • Done 方法可以降低计数器的值。他不接受任何参数,因此,它每执行一次计数器就减 1。

4. channel

channel 具体看这篇文章吧 channel介绍

之前的一篇博客已经讲的很清楚了。

5. 代码

简单了解完上述之后,我们开始写代码。

解释

既然是并发,那么我们就要写3个函数,去分别打印我们的dog,cat,fish了。

这里用dog进行举例

func dog(){
    
    
	fmt.Println("dog")
}

那我们的主函数就要启动goroutine去并发了。大概就是一下这种情况。

func main(){
    
    
	//...省略一些逻辑
	go dog()
	go cat()
	go fish()
	//...省略一些逻辑
}

那么我们先控制这三个的并发顺序,可以直接select去阻塞进行调试。

既然要控制并发顺序,我们就要可以用channel进行通信通知。我们先创建三个channel,用chan去传递信息。注意这里是传递无缓冲的channel,因为无缓冲是可以进行读写同步的。用来控制并发顺序最合适不过了。

dogChan, catChan, fishChan := make(chan bool), make(chan bool), make(chan bool)

dogChan 一开始赋值,并且dog打印完之后,给catChan通信,cat打印完之后,给fishChan通信,fish打印完后给dogChan通信。打完10次之后就停止。

比如这个传入dogChan 和 catChan 进行通信。把dogChan的取出,再将catChan的赋值,就可以不断进行循环调度了。

func dog(dogChan chan bool,catChan chan bool ) {
    
    
	for {
    
    
		select {
    
    
		case <-dogChan:
			fmt.Println("dog")
			catChan <- true
			break
		default:
			break
		}
	}
}

我们主程序可以用 sync.WaitGroup 来进行阻塞。当完成10次之后才Done掉,那么就完成了。

func fish(fishChan chan bool,dogChan chan bool ) {
    
    
	i := 0
	for {
    
    
		select {
    
    
		case <-fishChan:
			fmt.Println("fish")
			i++
			if i > 9 {
    
    
				wg.Done()
				return
			}
			dogChan <- true
			break
		default:
			break
		}
	}
}

完整

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func dog(dogChan chan bool,catChan chan bool ) {
    
    
	for {
    
    
		select {
    
    
		case <-dogChan:
			fmt.Println("dog")
			catChan <- true
			break
		default:
			break
		}
	}
}

func cat(catChan chan bool,fishChan chan bool ) {
    
    
	for {
    
    
		select {
    
    
		case <-catChan:
			fmt.Println("cat")
			fishChan <- true
			break
		default:
			break
		}
	}
}

func fish(fishChan chan bool,dogChan chan bool ) {
    
    
	i := 0
	for {
    
    
		select {
    
    
		case <-fishChan:
			fmt.Println("fish")
			i++ // 计数,打印完之后就溜溜结束了。
			if i > 9 {
    
    
				wg.Done()
				return
			}
			dogChan <- true
			break
		default:
			break
		}
	}
}

func main() {
    
    
	dogChan, catChan, fishChan := make(chan bool), make(chan bool), make(chan bool)
	wg.Add(1)
	go dog(dogChan, catChan)
	go cat(catChan, fishChan)
	go fish(fishChan, dogChan)
	dogChan <- true // 记得这里进行启动条件,不然就没法启动了。
	wg.Wait()
}

猜你喜欢

转载自blog.csdn.net/weixin_45304503/article/details/123032545