golang -- 如何让main goroutine等一等
目录
- 引言
- 一、sync.WaitGroup
- 二、channel
- 创建
- channle操作
- 缓冲
- 多返回值模式
- 单向通道
引言
在不做修饰的程序中,代码是串行执行的
串行、并发与并行串行:事物按照一定的发展顺序并发:同一时间段执行多个任务(一边吃饭一边看电视)并行:同一时刻执行多个任务(你和你的好朋友都在学习go语言)
对于这样一段代码
func hello() {fmt.Println("hello")
}
func main() {hello()fmt.Println("world")
}
输出
hello
world
如果启动一个goroutine
func hello() {fmt.Println("hello")
}
func main() {go hello()fmt.Println("world")
}
输出
world
为什么会出现这样的结果
这是因为在创建goroutine时需要花费一定时间,在这段时间内,main goroutine是继续执行的,如果main goroutine执行完成,就不会管其他的goroutine,程序直接退出,所以不会打印出hello
如果想要“hello”也打印出来,就要想办法让main goroutine 等一等
最简单粗暴的方法就是调用time.Sleep函数,延迟结束程序
func hello() {fmt.Println("hello")
}
func main() {hello()fmt.Println("world")time.Sleep(time.Second)
}
编译执行后打印
world
hello
为什么先打印world?
同样的,创建goroutine时需要花费一定时间,在这段时间内,main goroutine继续执行,理解为因为创建goroutine时花费了时间,所以goroutine执行起来比main goroutine慢
但是使用这种方法存在一定的问题,因为不知道程序执行具体需要多长时间
如果sleep时间过长,可能会存在 一段时间内,程序没有执行任务 的情况,这样就降低了效率
如果sleep时间过短,还是有可能打印不出goroutine中的语句
要更好的解决这个问题,有三种方法可以使用
- sync.WaitGroup
- channel
- Context
先不说第三种,因为还不会
一、sync.WaitGroup
使用sync.WaitGroup优化上面程序的代码是
var wg sync.WaitGroup //声明等待组变量func hello() {fmt.Println("hello")wg.Done() //当前goroutine执行完毕
}func main() {wg.Add(1) //记录需要等待的goroutine数量go hello()fmt.Println("world")wg.Wait() // 等待直到所有goroutine执行完成
}
编译执行后打印
world
hello
- sync.WaitGroup定义
type WaitGroup struct {noCopy noCopystate atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.sema uint32
}
不难而见,WaitGroup是一个结构体
sync包中提供了三种WaitGroup的方法
方法名 | 功能 |
---|---|
func (wg *WaitGroup) Add(delta int) | 记录要等待的协程的数量 |
func (wg *WaitGroup) Done() | 记录当前协程已执行完毕 |
func (wg *WaitGroup) Wait() | 等待子协程结束,否则阻塞 |
sync.WaitGroup内部可以理解为一个计数器,计数器的值可以增加和减少。
例如当我们启动了 N 个并发任务时,就将计数器值增加N;
每个任务完成时通过调用 Done 方法将计数器减1;
通过调用 Wait 来等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成
举一个通俗的例子,这里将计数器称为count
你室友喊你去打游戏,你给自己定了两个目标:复习二重积分和三重积分,完成这两个目标就去和室友打游戏
首先,你给自己定了两个目标(count = 2),复习完二重积分后,你还剩下一个目标没有完成(count = 1),这时候你的室友还要继续等你(wait),好了,又把三重积分复习完了(count = 0),这时候你就和室友去打游戏了
WaitGroup 通常适用于可动态调整协程数量的时候,例如事先知道协程的数量,又或者在运行过程中需要动态调整。
WaitGroup是结构体,作为函数参数传参时,应该传递指针而不是值;如果传递值,只是将WaitGroup拷贝后,对拷贝的WaitGroup进行修改,不会改变真正的WaitGroup的值,这可能会导致主协程一直阻塞等待,程序将无法正常运行
二、channel
这一部分是我在学习channel 时候的笔记加上自己的理解
参考🔗李文周的博客-Go语言基础之并发 🔗Golang 中文学习文档-并发
创建
- 声明
var 变量名称 chan 元素类型
chan:关键字
元素类型:是指通道中传递元素的类型
var ch chan int //传递整型的通道var ch1 chan string //传递string类型的通道
没有初始化之前通道是对应类型的零值
- 创建
用make创建
ch1 := make(chan int) //没有缓冲区(下面说缓冲)ch2 := make(chan int, 1) //缓冲区为1
- 关闭
使用内置的 close 关闭通道
close定义:
func close(c chan<- Type)
关闭ch通道:
close(ch)
用户必须发出一个关闭通道的指令,通道才会关闭
与文件操作不同,文件在结束操作后必须关闭,但是通道不必须关闭
channle操作
通道的操作有 发送(send)、接收(receive)和关闭(close)。
发送和接收都用符号 ‘<-’
- 发送
ch <-:表示对一个通道写入数据
ch <- 5 // 把5发送到ch中
- 接收
<- ch:表示对一个通道读取数据(直接看箭头指向区分这两种操作就可以)
- 单返回值
x := <- ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果
- 双返回值
value, ok := <-ch
value:通道中的值,如果被关闭返回对应的零值
ok:布尔类型的值,通道关闭时返回false,否则返回true
双返回值还可以用来判断通道是否关闭
-
判断通道是否被关闭:
示例:value, ok := <-ch
value:从通道中取出的值,如果通道被关闭则返回对应类型的零值
ok:通道ch关闭时返回false,否则返回true -
for range 接收值
通常用for range循环从通道中接收值
如果通道被关闭,会 在通道内的所有制被接收完毕后 自动退出循环
如果没有关闭,使用for range执行时会出错
ch4 := make(chan int, 4)ch4 <- 1ch4 <- 2ch4 <- 3ch4 <- 4close(ch4)for value := range ch4 {fmt.Println(value)}}func recv(c chan int) {ret := <-cfmt.Println("接收成功", ret)
}
输出
1
2
3
4
通道的发送接收操作可以理解为一个容器
如果容器有空间,就可以把物品放进容器;
如果容器空间满了,在容器中取出物品后,容器空间又有剩余,又可以把其他物品放入容器
关闭后的通道有以下特点:
- 对一个关闭的通道再发送值就会导致panic
- 对一个关闭的通道继续接收会一直获取值直到通道为空
- 对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值
- 关闭一个已经关闭的通道会导致panic
- 对已经关闭的通道再执行close也会引发panic
缓冲
- 无缓冲的通道(阻塞的通道)
ch1 := make(chan int) //ch1是一个无缓冲的通道ch1 <- 10fmt.Println("发送成功")
go fatal error: all goroutines are asleep - deadlock!
deadlock -- 表示程序中的goroutine都被挂起导致程序死锁了对一个无缓冲区通道执行发送操作,会发生阻塞对一个无缓冲通道执行接收操作,没有任何向通道中发送值的操作也会导致接受操作阻塞
应对阻塞通道的方法
- 创建goroutine
func recv(c chan int) {ret := <-cfmt.Println("接收成功", ret)}func main(){ch2 := make(chan int)go recv(ch2)ch2 <- 10 // 发送操作fmt.Println("发送成功")close(ch2)//fmt.Println(<-ch2) -- 报错了}
这段代码的过程
case1:如果先进行发送操作,发生堵塞,直到另一个goroutine执行接收操作
case2:如果先进行接收操作,发生堵塞,直到另一个goroutine执行发送操作
- 使用有缓冲的通道
ch3 := make(chan int, 1)ch3 <- 1fmt.Println("发送成功 ")x1 := <-ch3fmt.Println(x1) //输出1ch3 <- 2close(ch3)//对关闭的通道执行接收操作,会直到取完通道中的元素num0 := len(ch3) //len--获取通道中元素个数x2 := <-ch3num := len(ch3)fmt.Println(num0, x2, num) //输出 1 2 0
只要通道的容量大于0,那就是有缓冲的通道,通道的容量表示通道中最大能存放的元素数量。
当通道内一铀元素达到最大容量后,再向通道中执行发送操作就会阻塞(如果接收后再发送就不会了,相当于清空了)
len -- 获取通道内元素的数量
cap -- 获取通道的容量
多返回值模式
-
判断通道是否被关闭:
示例:value, ok := <-ch
value:从通道中取出的值,如果通道被关闭则返回对应类型的零值
ok:通道ch关闭时返回false,否则返回true -
for range 接收值
通常用for range循环从通道中接收值
如果通道被关闭,会 在通道内的所有制被接收完毕后 自动退出循环
如果没有关闭,使用for range执行时会出错
ch4 := make(chan int, 4)ch4 <- 1ch4 <- 2ch4 <- 3ch4 <- 4close(ch4)for value := range ch4 {fmt.Println(value)}}func recv(c chan int) {ret := <-cfmt.Println("接收成功", ret)
}
输出
1
2
3
4
单向通道
现在有两个函数
producer 函数 返回通道,并且执行发送操作
consume r函数从通道中接收值进行计算
func Producer() chan int {ch := make(chan int) //有没有缓冲值都可以//创建一个新的goroutine执行发送数据的任务go func() {for i := 0; i < 5; i++ {ch<-i}}close(ch)}()return ch
}func Consumer(ch chan int) int {sum := 0for value := range ch {sum += value}return sum
}
上面的代码没办法阻止在接收通道中执行发送操作,同理,没办法阻止在发送通道中执行接收操作
可以 限制参数或者返回值 来限制函数
<-chan int //只接收通道,只能接收不能发送
chan <-int //只发送通道,只能发送不能接收
改写成
func Producer1() <-chan int { //发送操作ch := make(chan int) //有没有缓冲值都可以//创建一个新的goroutine执行发送数据的任务go func() {for i := 0; i < 10; i++ {//有 1 3 5 7 9if i%2 == 1 {ch <- i}}close(ch)}()return ch
}func Consumer1(ch <-chan int) int { //接收操作sum := 0for value := range ch {sum += value}return sum
}func main() {//在函数传参及任何赋值操作中全向通道(正常通道)可以转换为单向通道,但是没办法反向转换(单项通道没办法转换成全向通道)ch1 := make(chan int, 1)ch1 <- 10close(ch1)Consumer1(ch1) //在传参时将ch1转为单项通道ch2 := make(chan int, 1)ch2 <- 4 //向ch2中发送4var ch3 <-chan int //声明一个通道 只接收ch3 = ch2 //变量赋值时将ch2转换为单向通道<-ch3 //接收操作
}