文章目录
参考
并发控制
我们考虑这么一种场景,协程A执行过程中需要创建子协程A1、A2、A3…An,协程A创建完子协程后就等待子协程退出。
针对这种场景,GO提供了三种解决方案:
- Channel: 使用channel控制子协程
- WaitGroup : 使用信号量机制控制子协程
- Context: 使用上下文控制子协程
三种方案各有优劣,比如Channel优点是实现简单,清晰易懂,WaitGroup优点是子协程个数动态可调整,Context优点是对子协程派生出来的孙子协程的控制。
缺点是相对而言的,要结合实例应用场景进行选择。
通道 channel
1. 使用必须要初始化(同 map 一样),这是声明个 int 类型的通道
ch:=make(chan int)
2. 收发信息( <- 符号固定,ch 在左为写入通道,ch 在右 为从通道读)
ch <- 2 //发送数值2给这个通道
x:=<-ch //从通道里读取值,并把读取的值赋值给x变量
<-ch //从通道里读取值,然后忽略
- 自己总结记忆法(从右向左读法)
- ch <- ,读为「向通道」,理解为向通道传输,那么就是发送
- <- ch ,读为「通道向」,理解为通道向外传,那么就是取值
3. 关闭通道
close(ch)
4. 有无缓冲通道
ch:=make(chan int) 无缓冲
ch:=make(chan int,0) 同上,无缓冲
ch:=make(chan int,2) 有缓冲
- 无缓冲通道,又称为「同步通道」,因为没有存储能力,因此要求「发送」和「接收」goroutine 必须同时准备好
- 若没有同时准备好,那么先执行的操作就会「阻塞等待」,知道另一个相对应的操作准备好为止,因此也称之为「同步」
- 有缓冲的通道,队列满的时候,「发送」操作阻塞;队列空的时候,「接收」操作会阻塞
- 因此此方式可解耦发送和接收操作
- 如何知道通道中有多少元素?
- cap(ch) 返回通道的最大数量
- len(ch) 返回通道中现在有多少个元素
5. 单向通道
var send chan<- int //只能发送
var receive <-chan int //只能接收
举例
控制子协程
-
使用channel来控制子协程的优点是实现简单
-
缺点是当需要大量创建协程时就需要有相同数量的channel,而且对于子协程继续派生出来的协程不方便控制。
-
后面继续介绍的WaitGroup、Context看起来比channel优雅一些,在各种开源组件中使用频率比channel高得多。
// 上面程序通过创建N个channel来管理N个协程,每个协程都有一个channel用于跟父协程通信,父协程创建完所有协程后等待所有协程结束。
// 这个例子中,父协程仅仅是等待子协程结束,其实父协程也可以向管道中写入数据通知子协程结束,这时子协程需要定期地探测管道中是否有消息出现。
package main
import (
"time"
"fmt"
)
func Process(ch chan int) {
//Do some work...
time.Sleep(time.Second)
ch <- 1 //管道中写入一个元素表示当前协程已结束
}
func main() {
channels := make([]chan int, 10) //创建一个10个元素的切片,元素类型为channel
for i:= 0; i < 10; i++ {
channels[i] = make(chan int) //切片中放入一个channel
go Process(channels[i]) //启动协程,传一个管道用于通信
}
for i, ch := range channels { //遍历切片,等待子协程结束
<-ch
fmt.Println("Routine ", i, " quit!")
}
}
模拟管道
// 这里例子中我们定义两个通道one和two,然后按照顺序,先把100发送给通道one,然后用另外一个goroutine从one接收值,
// 再发送给通道two,最终在主goroutine里等着接收打印two通道里的值,这就类似于一个管道的操作,
// 把通道one的输出,当成通道two的输入,类似于接力赛一样。
func main() {
one := make(chan int)
two := make(chan int)
go func() {
one<-100
}()
go func() {
v:=<-one
two<-v
}()
fmt.Println(<-two)
}
WaitGroup
简单说来,WaitGroup
通常用于等待一组“工作协程”结束的场景,其内部维护两个计数器,这里把它们称为“工作协程”计数器和“坐等协程”计数器,
WaitGroup
对外提供的三个方法分工非常明确:
Add(delta int)
方法用于增加“工作协程”计数,通常在启动新的“工作协程”之前调用;Done()
方法用于减少“工作协程”计数,每次调用递减1
,通常在“工作协程”内部且在临近返回之前调用;Wait()
方法用于增加“坐等协程”计数,通常在所有”工作协程”全部启动之后调用;
Done()
方法除了负责递减“工作协程”计数以外,还会在“工作协程”计数变为0
时检查“坐等协程”计数器并把“坐等协程”唤醒。
需要注意的是,Done()
方法递减“工作协程”计数后,如果“工作协程”计数变成负数时,将会触发panic
,这就要求Add()
方法调用要早于Done()
方法。
此外,通过Add()
方法累加的“工作协程”计数要与实际需要等待的“工作协程”数量一致,否则也会触发panic
。
当“工作协程”计数多于实际需要等待的“工作协程”数量时,“坐等协程”可能会永远无法被唤醒而产生列锁,此时,Go运行时检测到死锁会触发panic
,
当“工作协程”计数小于实际需要等待的“工作协程”数量时,Done()
会在“工作协程”计数变为负数时触发panic
。
使用示例
简单的说,下面程序中wg内部维护了一个计数器:
- 启动goroutine前将计数器通过Add(2)将计数器设置为待启动的goroutine个数。
- 启动goroutine后,使用Wait()方法阻塞自己,等待计数器变为0。
- 每个goroutine执行结束通过Done()方法将计数器减1。
- 计数器变为0后,阻塞的goroutine被唤醒。
其实WaitGroup也可以实现一组goroutine等待另一组goroutine,这有点像玩杂技,很容出错,如果不了解其实现原理更是如此。实际上,WaitGroup的实现源码非常简单。
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!")
}
实现原理
预备知识 —— 信号量
信号量是Unix系统提供的一种保护共享资源的机制,用于防止多个线程同时访问某个资源。
可简单理解为信号量为一个数值:
- 当信号量>0时,表示资源可用,获取信号量时系统自动将信号量减1;
- 当信号量==0时,表示资源暂不可用,获取信号量时,当前线程会进入睡眠,当信号量为正时被唤醒;
由于WaitGroup实现中也使用了信号量,在此做个简单介绍。
WaitGroup 数据结构
type WaitGroup struct {
state1 [3]uint32
}
// state1是个长度为3的数组,其中包含了state和一个信号量,而state实际上是两个计数器:
// - counter: 当前还未执行结束的goroutine计数器
// - waiter count: 等待goroutine-group结束的goroutine数量,即有多少个等候者
// - semaphore: 信号量
// 考虑到字节是否对齐,三者出现的位置不同,为简单起见,依照字节已对齐情况下,三者在内存中的位置如下所示:
接口
// 简单描述
// - counter: 现在正在执行的 goroutine 数量
// - waiter count: 等待结束后,要运行的 goroutine 数量
// - semaphore: 信号量,>0 表示资源可用,==0 表示资源暂不可用
// WaitGroup对外提供三个接口:
// - Add(delta int): 将delta值加到counter中
// - Wait(): waiter递增1,并阻塞等待信号量semaphore
// - Done(): counter递减1,按照waiter数值释放相应次数信号量
--------
// Add(delta int) 方法
// Add()做了两件事,一是把delta值累加到counter中,因为delta可以为负值,也就是说counter有可能变成0或负值
// 所以第二件事就是当counter值变为0时,根据waiter数值释放等量的信号量,把等待的goroutine全部唤醒,如果counter变为负值,则panic.
func (wg *WaitGroup) Add(delta int) {
statep, semap := wg.state() //获取state和semaphore地址指针
state := atomic.AddUint64(statep, uint64(delta)<<32) //把delta左移32位累加到state,即累加到counter中
v := int32(state >> 32) //获取counter值
w := uint32(state) //获取waiter值
if v < 0 { //经过累加后counter值变为负值,panic
panic("sync: negative WaitGroup counter")
}
//经过累加后,此时,counter >= 0
//如果counter为正,说明不需要释放信号量,直接退出
//如果waiter为零,说明没有等待者,也不需要释放信号量,直接退出
if v > 0 || w == 0 {
return
}
//此时,counter一定等于0,而waiter一定大于0(内部维护waiter,不会出现小于0的情况),
//先把counter置为0,再释放waiter个数的信号量
*statep = 0
for ; w != 0; w-- {
runtime_Semrelease(semap, false) //释放信号量,执行一次释放一个,唤醒一个等待者
}
}
-------
// Wait() 方法
// Wait()方法也做了两件事,一是累加waiter, 二是阻塞等待信号量
func (wg *WaitGroup) Wait() {
statep, semap := wg.state() //获取state和semaphore地址指针
for {
state := atomic.LoadUint64(statep) //获取state值
v := int32(state >> 32) //获取counter值
w := uint32(state) //获取waiter值
if v == 0 { //如果counter值为0,说明所有goroutine都退出了,不需要待待,直接返回
return
}
// 使用CAS(比较交换算法)累加waiter,累加可能会失败,失败后通过for loop下次重试
if atomic.CompareAndSwapUint64(statep, state, state+1) {
runtime_Semacquire(semap) //累加成功后,等待信号量唤醒自己
return
}
}
}
-------
// Done() 方法
// Done()只做一件事,即把counter减1,我们知道Add()可以接受负值,所以Done实际上只是调用了Add(-1)。
func (wg *WaitGroup) Done() {
wg.Add(-1)
}
Context
简述作用
- 与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine
- context翻译成中文是”上下文”,即它可以控制一组呈树状结构的goroutine,「每个goroutine拥有相同的上下文」。

实现原理
接口定义
// 源码包 src/context/context.go:Context
type Context interface {
// 该方法返回一个deadline和标识是否已设置deadline的bool值,
// 如果没有设置deadline,则ok == false,此时deadline为一个初始值的time.Time值
Deadline() (deadline time.Time, ok bool)
// 该方法返回一个channel,需要在select-case语句中使用,如”case <-context.Done():”。
// 当context关闭后,Done()返回一个被关闭的管道,关闭的管道仍然是可读的,据此goroutine可以收到关闭请求;
// 当context还未关闭时,Done()返回nil。
Done() <-chan struct{}
// 该方法描述context关闭的原因。关闭原因由context实现控制,不需要用户设置。
// 比如Deadline context,关闭原因可能是因为deadline,也可能提前被主动关闭,那么关闭原因就会不同:
// - 因deadline关闭:“context deadline exceeded”;
// - 因主动关闭: “context canceled”。
// 当context关闭后,Err()返回context的关闭原因;
// 当context还未关闭时,Err()返回nil
Err() error
// 有一种context,它不是用于控制呈树状分布的goroutine,而是「用于在树状分布的goroutine间传递信息」。
// Value()方法就是用于此种类型的context,该方法根据key值查询map中的value。具体使用后面示例说明。
Value(key interface{}) interface{}
}
空 context
// context包中定义了一个空的context, 名为emptyCtx,用于context的根节点,
// 空的context只是简单的实现了Context,本身不包含任何值,仅用于其他context的父节点。
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
// context包中定义了一个公用的emptCtx全局变量,名为background,可以使用context.Background()获取它,实现代码如下所示:
var background = new(emptyCtx)
func Background() Context {
return background
}
四种方法实现不同类型 context
context包提供了4个方法创建不同类型的context,使用这四个方法时如果没有父context,都需要传入backgroud,即backgroud作为其父节点:
- WithCancel()
- WithDeadline()
- WithTimeout()
- WithValue()
context包中实现Context接口的struct,除了emptyCtx外,还有cancelCtx、timerCtx和valueCtx三种,正是基于这三种context实例,实现了上述4种类型的context。
context包中各context类型之间的关系,如下图所示:
小结
- Context仅仅是一个接口定义,根据实现的不同,可以衍生出不同的context类型;
- cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;
- timerCtx实现了Context接口,通过WithDeadline()和WithTimeout()创建timerCtx实例;
- valueCtx实现了Context接口,通过WithValue()创建valueCtx实例;
- 三种context实例可互为父节点,从而可以组合成不同的应用形式;
cancelCtx —— WithCancel()方法实现
使用
WithCancel()方法作了三件事:
- 初始化一个cancelCtx实例
- 将cancelCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
- 返回cancelCtx实例和cancel()方法
其实现源码如下所示:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c) //将自身添加到父节点
return &c, func() { c.cancel(true, Canceled) }
}
这里将自身添加到父节点的过程有必要简单说明一下:
- 如果父节点也支持cancel,也就是说其父节点肯定有children成员,那么把新context添加到children里即可;
- 如果父节点不支持cancel,就继续向上查询,直到找到一个支持cancel的节点,把新context添加到children里;
- 如果所有的父节点均不支持cancel,则启动一个协程等待父节点结束,然后再把当前context结束。
实现原理
// 源码包中src/context/context.go:cancelCtx 定义了该类型context:
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
// children中记录了由此context派生的所有child,此context被cancel时会把其中的所有child都cancel掉。
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
// cancelCtx与deadline和value无关,所以「只需要实现Done()和Err()外露接口即可」。
---------
// Done()接口实现
// 按照Context定义,「Done()接口只需要返回一个channel即可」,对于cancelCtx来说只需要返回成员变量done即可。
// 由于cancelCtx没有指定初始化函数,所以cancelCtx.done可能还未分配,所以需要考虑初始化。
// cancelCtx.done会在context被cancel时关闭,所以cancelCtx.done的值一般经历如下三个阶段:
// nil –> chan struct{} –> closed chan。
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
---------
// Err()接口实现
// 按照Context定义,Err()「只需要返回一个error告知context被关闭的原因」。对于cancelCtx来说只需要返回成员变量err即可。
// cancelCtx.err默认是nil,在context被cancel时指定一个error变量: var Canceled = errors.New("context canceled")
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
----------
// cancel()接口实现
// cancel()内部方法是理解cancelCtx的最关键的方法,「其作用是关闭自己和其后代」
// 其后代存储在cancelCtx.children的map中,其中key值即后代对象,value值并没有意义,这里使用map只是为了方便查询而已
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
c.mu.Lock()
c.err = err //设置一个error,说明关闭原因
close(c.done) //将channel关闭,以此通知派生的context
for child := range c.children { //遍历所有children,逐个调用cancel方法
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent { //正常情况下,需要将自己从parent删除
removeChild(c.Context, c)
}
}
timerCtx —— WithDeadline()方法实现
使用
WithDeadline()方法实现步骤如下:
- 初始化一个timerCtx实例
- 将timerCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
- 启动定时器,定时器到期后会自动cancel本context
- 返回timerCtx实例和cancel()方法
也就是说,timerCtx类型的context不仅支持手动cancel,也会在定时器到来后自动cancel。
实现
// 源码包中src/context/context.go:timerCtx 定义了该类型context:
type timerCtx struct {
cancelCtx // 继承上面的结构定义
timer *time.Timer // Under cancelCtx.mu.
// timerCtx在cancelCtx基础上『增加了deadline用于标示自动cancel的最终时间』,而timer就是一个触发自动cancel的定时器。
deadline time.Time
}
// 由此,「衍生出WithDeadline()和WithTimeout()」。实现上这两种类型实现原理一样,只不过使用语境不一样:
// - deadline: 指定最后期限,比如context将2018.10.20 00:00:00之时自动结束
// - timeout: 指定最长存活时间,比如context将在30s后结束。
// 对于接口来说,『timerCtx在cancelCtx基础上还需要实现Deadline()和cancel()方法』,其中cancel()方法是重写的。
--------
// Deadline()接口实现
// Deadline()方法「仅仅是返回timerCtx.deadline而矣」。而timerCtx.deadline是WithDeadline()或WithTimeout()方法设置的。
--------
// cancel()方法基本继承cancelCtx,只需要额外把timer关闭。
// timerCtx被关闭后,timerCtx.cancelCtx.err将会存储关闭原因:
// - 如果deadline到来之前手动关闭,则关闭原因与cancelCtx显示一致;
// - 如果deadline到来时自动关闭,则原因为:”context deadline exceeded”
--------
// WithDeadline()方法实现步骤如下:
// - 初始化一个timerCtx实例
// - 将timerCtx实例添加到其父节点的children中(如果父节点也可以被cancel的话)
// - 启动定时器,定时器到期后会自动cancel本context
// - 返回timerCtx实例和cancel()方法
// 也就是说,timerCtx类型的context不仅支持手动cancel,也会在定时器到来后自动cancel。
-------
// WithTimeout()实际调用了WithDeadline,二者实现原理一致。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
valueCtx —— WithValue()方法实现
使用
// 上例main()中通过WithValue()方法获得一个context,需要指定一个父context、key和value。
// 然后通将该context传递给子协程HandelRequest,子协程可以读取到context的key-value。
// 注意:本例中子协程无法自动结束,因为context是不支持cancle的,也就是说<-ctx.Done()永远无法返回。
// 如果需要返回,需要在创建context时指定一个可以cancel的context作为父节点,使用父节点的cancel()在适当的时机结束整个context。
package main
import (
"fmt"
"time"
"context"
)
func HandelRequest(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("HandelRequest Done.")
return
default:
fmt.Println("HandelRequest running, parameter: ", ctx.Value("parameter"))
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx := context.WithValue(context.Background(), "parameter", "1")
go HandelRequest(ctx)
time.Sleep(10 * time.Second)
}
实现
// 源码包中src/context/context.go:valueCtx 定义了该类型context:
// valueCtx只是在Context基础上「增加了一个key-value对」,用于在各级协程间传递一些数据。
type valueCtx struct {
Context
key, val interface{}
}
// 由于valueCtx既不需要cancel,也不需要deadline,那么只需要实现Value()接口即可。
--------
// Value()接口实现
// 由valueCtx数据结构定义可见,valueCtx.key和valueCtx.val分别代表其key和value值。 实现也很简单:
// 这里有个细节需要关注一下,即当前context查找不到key时,会向父节点查找,如果查询不到则最终返回interface{}。也就是说,可以通过子context查询到父的value值。
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
---------
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
return &valueCtx{parent, key, val}
}