Go中的goroutine之间没有父与子的关系,也就没有所谓子进程退出后的通知机制,多个goroutine都是平行地被调度,多个goroutine如何协作工作涉及通信、同步、通知和退出四个方面。
Go语言在语法上处理某个goroutine退出通知机制很简单。但是遇到复杂的并发结构处理起来就显得力不从心。实际编程中goroutine会拉起新的goroutine,新的goroutine又会拉起另一个新的goroutine,最终形成一个树状的结构,由于goroutine里并没有父子的概念,这个树状的结构只是在程序员头脑中抽象出来的,程序的执行模型并没有维护这么一个树状结构。怎么通知这个树状上的所有goroutine退出?仅依靠语法层面的支持显然比较难处理。为此G01.7
提供了一个标准库context来解决这个问题。它提供两种功能:退出通知和元数据传递context库的设计目的就是跟踪goroutine调用,在其内部维护一个调用树,并在这些调用树中传递通知和元数据。
context库的设计目的就是跟踪goroutine调用树,并在这些goroutine调用树中传递通知和元数据。
在介绍context包之前,先理解context包的整体工作机制:第一个创建Context的goroutine被称为root节点。root节点负责创建一个实现Context接口的具体对象,并将该对象作为参数传递到其新拉起的goroutine,下游的goroutine可以继续封装该对象,再传递到更下游的goroutine。Context对象在传递的过程中最终形成一个树状的数据结构,这样通过位于root节点(树的根节点)的Context对象就能遍历整个Context对象树,通知和消息就可以通过root节点传递出去,实现了上游goroutine对下游goroutine的消息传递。
Context是一个基本接口,所有的Context对象都要实现该接口,context的使用者在调用接口中都使用Context作为参数类型。
type Context interface {
//如果Context实现了超时控制,则该方法返回ok true,deadlline为超时时间,否则ok为false
Deadline() (deadline time.Time, ok bool)
//后端被调的goroutine应该监听该方法返回的chan,以便及时释放资源
Done() <-chan struct{}
//Done返回的chan收到通知的时候,才可以访问Err()获取取消原因
Err() error
//可以访问上游goroutine传递给下游goroutine的值
Value(key interface{}) interface{}
}
canceler接口是一个扩展接口,规定了取消通知的Context具体类型需要实现的接口。context包中的具体类型cancelCtx和timerCtx都实现了该接口。示例如下:
//一个context对象如果实现了canceler接口,则可以被取消
type canceler interface {
//创建cancel接口实例的goroutine调用cancel方法通知后续创建的goroutine退出
cancel(removeFromParent bool, err error)
//Done方法返回chan需要后端goroutine来监听,并及时退出
Done() <-chan struct{}
}
emptyCtx实现了Context接口,但不具备任何功能,因为其所有的方法都是空实现。其存在的目的是作为Context对象树的根(root节点)。因为context包,的使用思路就是不停地调用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
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
package定义了两个全局变量和两个封装函数,返回两个emptyCtx实例对象,实际使用时候通过调用这两个封装函数来构造Context的root节点:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
cancelCtx是一个实现了Context接口的具体类型,同时实现了conceler接口。conceler具有退出通知方法。注意退出通知机制不但能通知自己,也能逐层通知其children节点。示例如下:
//cancelCtx可以被取消,cancelCtx取消时会同时取消所有实现canceler接口的孩子节点
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
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
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}
func (c *cancelCtx) String() string {
return fmt.Sprintf("%v.WithCancel", c.Context)
}
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
//显示通知自己
close(c.done)
}
//循环调用children的cancel函数,由于parent已经取消,所以此时child调用cancel传入的是false
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
timerCtx是一个实现了Context接口的具体类型,内部封装了cancelCtx类型实例,同时有一个deadline变量,用来实现定时退出通知。示例如下:
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) String() string {
return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, time.Until(c.deadline))
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
yalueCtx是一个实现了Context接口的具体类型,内部封装了Context接口类型,同时封装了一个k/v的存储变量。valueCtx可用来传递通知信息。示例如下:
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) String() string {
return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
下面这两个函数是构造Context取消树的根节点对象,根节点对象用作后续With包装函数的实参。
func Background()Context
func TODO()Context
With包装函数用来构建不同功能的Context具体对象。
func WithCancel(parent Context)(ctx Context,cancel CancelFunc)
func WithDeadline(parent Context,deadline time.Time)(Context,CancelFunc)
func WithTimeout (parent Context,timeout time.Duration)
(Context, CancelFunc)
func WithValue(parent Context,key,val interface(})Context
这些函数都有一个共同的特点一parent参数,其实这就是实现Context通知树的必备条件。在goroutine的调用链中,Context的实例被逐层地包装并传递,每层又可以对传进来的Context实例再封装自己所需的功能,整个调用树需要一个数据结构来维护,这个维护逻辑在这些包装函数内部实现。
前面描述的With开头的构造函数是给外部程序使用的API接口函数。Context具体对象的链条关系是在Wth函数的内部维护的。现在分析一下With函数内部使用的通用函数。
func propagateCancel(parent Context,child canceler)
有如下几个功能:
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
//维护parent和child的关系
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
func parentCancelCtx(parent Context) (*cancelCtx, bool)
判断parent中是否封装cancelCtx的字段,或者接口里面存放的底层类型是否是cancelCtx类型:
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}
func removeChild(parent Context,child canceler)
如果parent封装cancelCtx类型字段,或者接口里面存放的底层类型是cancelCtx类型,则将其构造树上的child节点删除。示例如下:
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
package main
import (
"context"
"fmt"
"time"
)
func main() {
//使用context.Background()构建一个WithCancel类型的上下文
ctxa, cancel := context.WithCancel(context.Background())
//work模拟运行并监测前端的退出通知
go work(ctxa, "work1")
//使用WithDeadline包装前面的上下文对象ctxa
tm := time.Now().Add(3 * time.Second)
ctxb, _ := context.WithDeadline(ctxa, tm)
go work(ctxb, "work2")
//使用WithValue包装前面的上下文对象ctxb
oc := otherContext{ctxb}
ctxc := context.WithValue(oc, "key", "andes, pass from main ")
go workWithValue(ctxc, "work3")
//故意sleep10秒,让work2、work3超时退出
time.Sleep(10 * time.Second)
//显示调用work1的cancel方法通知其退出
cancel()
//等待work1打印退出信息
time.Sleep(5 * time.Second)
fmt.Println("main stop")
}
type otherContext struct {
context.Context
}
func work(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s get msg to cancel\n", name)
return
default:
fmt.Printf("%s is running \n", name)
time.Sleep(1 * time.Second)
}
}
}
//等待前端的退出通知,并视图获取Context传递的数据
func workWithValue(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s get msg to cancel\n", name)
return
default:
value := ctx.Value("key").(string)
fmt.Printf("%s is running value=%s \n", name, value)
time.Sleep(1 * time.Second)
}
}
}
//结果
work1 is running
work2 is running
work3 is running value=andes, pass from main
work1 is running
work2 is running
work3 is running value=andes, pass from main
work1 is running
work3 is running value=andes, pass from main
work2 is running
work1 is running
work3 get msg to cancel
work2 get msg to cancel
work1 is running
work1 is running
work1 is running
work1 is running
work1 is running
work1 is running
work1 get msg to cancel
main stop
使用Context的一般流程如下:
func Background()Context
func TODO()Context
func WithCancel(parent Context)(ctx Context,cancel CancelFunc)
func WithTimeout (parent Context,timeout time.Duration)(Context,CancelFunc)
func WithDeadline(parent Context,deadline time.Time)(Context,CancelFunc)
func Withvalue(parent Context,key,val interface())Context
首先要清楚使用context包主要是解决goroutine的通知退出,传递数据是其一个额外功能。可以使用它传递一些元信息,总之使用context传递的信息不能影响正常的业务流程,程序不要期待在context中传递一些必需的参数等,没有这些参数,程序也应该能正常工作。
context包提供的核心的功能是多个goroutine之间的退出通知机制,传递数据只是一个辅助功能,应谨慎使用context传递数据。