持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情
场景:协程A执行过程中需要创建子协程A1、A2、A3...An,协程A创建完子协程后就等待子协程退出。 针对这种场景,GO提供了三种解决方案:
- Channel: 使用channel控制子协程
- WaitGroup : 使用信号量机制控制子协程
- Context: 使用上下文控制子协程
一个程序启动,就会有对应的进程被创建,同时进程也会移动一个线程,这个线程叫做主线程。如果主线程结束,那么整个程序就退出了。有了主线程,就可以在主线程程里启动很多其他线程,就有了多线程的并发
但是 Go 里面没有线程的概念,只有协程,相对于线程更加轻量,一个程序可以随意启动成千上万个协程,而协程是被 Go runtime 调度, Go 自己决定同时执行多少个协程,什么时候执行哪几个
Go 语言中提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是体长通过 channel 发送接收消息的方式进行数据传递,而不是通过修改同一个变量。
通道
如果启动多个 goroutine, 它们之间的通信就需要 channel 来实现,通道主要用于共享内存
本质相当于一个 FIFO 队列
优点是实现比较简单,缺点是当需要大量创建协程的时候就需要相同数量的通道,对于子协程继续派生处理啊的协程不方便控制
创建方法
package main
import "fmt"
func main() {
ch1 := make(chan int, 3)
ch1 <- 2
ch1 <- 1
ch1 <- 3
elem1 := <-ch1
fmt.Printf("The first element received from channel ch1: %v\n",
elem1)
}
其中 chan 表示通道,int 表示类型,而 3 表示通道的容量,也就是通道最多可以缓存多少个元素值
缓冲通道和非缓冲通道,其中缓冲通道的容量大于0,非缓冲通道的容量等于0,他们的区别在于有着不同的数据传递方式
发送语句使用箭头,接收语句也是使用箭头
发送和接收的特性
- 对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
- 发送操作和接收操作中对元素值的处理都是不可分割的。
- 发送操作在完全完成之前会被阻塞。接收操作也是如此。
简单使用
func main() {
ch:=make(chan string)
go func() {
fmt.Println("飞雪无情")
ch <- "goroutine 完成"
}()
fmt.Println("我是 main goroutine")
v:=<-ch
fmt.Println("接收到的chan中的值为:",v)
}
结果是
我是 main goroutine
飞雪无情
接收到的chan中的值为: goroutine 完成
因为通过 make 创建的 chan 中没有值,而 main goroutine 又想从 chan 中获取值,获取不到就一直等待,等到另一个 goroutine 向 chan 发送值为止。所以程序不会在新的 goroutine 完成之前退出了
缓冲管道和无缓冲管道
无缓冲管道:容量为0,不存储任何数据,发送和接收操作是同时进行的,所以成为同步管道
缓冲管道:
cacheCh:=make(chan int,5)
- 内部有一个缓冲队列
- 发送操作是向队列的尾部插入元素,如果队列已满,则阻塞等待,直到另一个 goroutine 执行,接收操作释放队列的空间;
- 接收操作是从队列的头部获取元素并把它从队列中删除,如果队列为空,则阻塞等待,直到另一个 goroutine 执行,发送操作插入新的元素。
关闭管道
使用内置函数 close
如果一个 channel 被关闭了,就不能向里面发送数据了,如果发送的话,会引起 painc 异常。但是还可以接收 channel 里的数据,如果 channel 里没有数据的话,接收的数据是元素类型的零值
单向管道
只接收不发送
Goroutine
sync.WaitGroup
场景:某个 goroutine 需要等待其他几个 goroutine 全部完成
优点是 协程个数动态可调整
- 使用方法
- wg.Add():main协程通过调用 wg.Add(delta int) 设置worker协程的个数,然后创建worker协程;
- wg.Done():worker协程执行结束以后,都要调用 wg.Done(),表示做完任务,goroutine减1;
- wg.Wait() :main协程调用 wg.Wait() 且被block,直到所有worker协程全部执行结束后返回。
- 针对可能panic的goroutine,可以使用defer wg.Done()来结束goroutine。
- 例子
package main
import (
"fmt"
"time"
"sync"
)
func main() {
var wg sync.WaitGroup
wg.Add(2) //设置计数器,数值即为goroutine的个数
go func() {
//Do some work
time.Sleep(1*time.Second)
fmt.Println("Goroutine 1 finished!")
wg.Done() //goroutine执行结束后将计数器减1
}()
go func() {
//Do some work
time.Sleep(2*time.Second)
fmt.Println("Goroutine 2 finished!")
wg.Done() //goroutine执行结束后将计数器减1
}()
wg.Wait() //主goroutine阻塞等待计数器变为0
fmt.Printf("All Goroutine finished!")
}
简单的说,上面程序中wg内部维护了一个计数器:
- 启动goroutine前将计数器通过Add(2)将计数器设置为待启动的goroutine个数。
- 启动goroutine后,使用Wait()方法阻塞自己,等待计数器变为0。
- 每个goroutine执行结束通过Done()方法将计数器减1。
- 计数器变为0后,阻塞的goroutine被唤醒。
- tips
- Add()操作必须早于Wait(), 否则会panic;
- Add()设置的值必须与实际等待的goroutine个数一致,否则会panic
Context
用于 goroutine 派生出子 goroutine,这时候用 waitGroup 就不太容易,因为子 goroutine 的个数不确定