goroutine和channel 如何控制并发顺序?
Posted 小生凡一
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
select
和switch
很像,它不需要输入参数,并且仅仅被使用在通道操作上。
select 语句被用来执行多个通道操作的一个和其附带的 case 块代码。
我们知道 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()
以上是关于goroutine和channel 如何控制并发顺序?的主要内容,如果未能解决你的问题,请参考以下文章
Golang 并发: goroutine and channel
Golang 并发: goroutine and channel