您的位置 首页 golang

浅析context


为什么会有这个包

我们其实知道goroutine是没有父子关系,也没有先后顺序的,所以也就没有了我们常说的子进程退出后的通知机制。那么成百上千的goroutine如何协同工作:通信,同步,退出,通知

1.通信:goroutine 的通知就是依靠chan
2.同步:goroutine如何同步其实我们可以通过无缓冲的channel和sync包下的waitgroup机制来进行同步
3.通知:goroutine间如何通知呢,其实通知不同于通信,通知更多的是管理和控制流数据。这个可以用两个chan来进行管控,一个进行业务流交互,另外一个用作通知做入库之类的操作,但是并不通用,这个管控的难度会随业务复杂度增加而增加
4.退出:这个在我上篇文章其实有单独提出来说的,等待退出机制,借助select 的广播机制实现退出

其实上面看似好像每一个部分都能有解决方案,但是实际拎出来讲一下如果每个goroutine退出都要写一个等待退出,那么go的便捷性是不是完完全全损失掉了。实际编码过程中goroutine没有父子关系,goroutine多开goroutine,最终形成一个树状调用结构,那么这里就有个问题,我在一个goroutine中如何知道另外一个goroutine是否退出呢,这就是大型项目必须要考虑的东西了。

context 起到了什么样的作用

1.退出通知机制,通知可以传递到整个goroutine调用树上的每一个goroutine
2.传递数据,数据可以传递给整个gortouine调用树上的每一个goroutine

基本数据结构

整体工作机制:创建第一个context的goroutine 被称为root节点,root节点负责创建一个context接口的具体对象,并将对象作为参数传递到新的goroutine。下游的goroutine可以继续封装该对象。这样的传递过程就生成了一个树状结构。此时root节点就可以传递消息到下游goroutine,

context 接口

type Context interface {    //如果context 实现了超时控制,则此方法这返回ok true,deadline为超时时间,否则ok为false    Deadline() (deadline time.Time, ok bool)    //后端被调的goroutine应该返回监听的方法返回的chan 以便及时释放资源    Done() <-chan struct{}    //Done返回的chan收到的通知的时候,才可以访问此方法为什么被取消0    Err() error    //可以访问上游的goroutine 传递给下游的goroutine的值    Value(key interface{}) interface{}}

Cancer 接口

type canceler interface {    //一个context 如果被实现了cancel接口,则可以被取消的        //创建cancel接口实例的goroutine 调用cancel方法通知后续创建goroutine退出    cancel(removeFromParent bool, err error)    //Done方法需要chan 返回goroutine来监听,并及时退出    Done() <-chan struct{}}

empty Context

我们平常使用的方法如下,其实空的节点最大的特点就是形成root节点,emptyctx所有的方法都是空的,并不具备任何功能。因为context包的使用思路就是不停的调用context包提供的包装函数来创建具有特殊功能的context实例,每一个context都以上一个context为参照对象,最终形成一个树状结构

func main()  {    c :=context.TODO()}type emptyCtx intfunc (*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"}var (    background = new(emptyCtx)    todo       = new(emptyCtx))

cancel context

cancelCtx 是一个实现了canceler接口,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) Value(key interface{}) interface{} {    if key == &cancelCtxKey {        return c    }    return c.Context.Value(key)}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()    err := c.err    c.mu.Unlock()    return err}func (c *cancelCtx) String() string {    return contextName(c.Context) + ".WithCancel"}// 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)    }    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

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, c.deadline.Sub(time.Now()))}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()}

valueCtx

valueCtx 是实现了一个context接口的具体类型,内部封装了Context接口类型,同时封装了一个key,value存储变量,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)}

API函数

api函数则不做细讲,具体可通过编译器查看其返回参数

1. func Background()context2.  func Todo()context3.withCancel(parent Context)(ctx Context,cancel CancelFunc)4.withDeadline(parent Context)(ctx Context,deadline time.Time)5.withTimeout(parent Context,timeout time.Duration)(ctx Context,timeout time.Duration)6.withValue(parent Context,key,value interface{})context

辅助函数

上面的Api函数是给外部创建ctx对象结构的api的话,那么其内部有些通用函数,我们可以来讲一讲

1.func propagateCancel(parent Context, child canceler)

1.判断parentDone方法是否为nil,如果是nil,那么说明parent是一个可取消的Context对象,也就灭有所谓的取消构造树,说明child就是取消构造树的根
2.如果parent方法Done返回不是nil,那么向上回溯自己的祖先是否为cancelCtx的类型实例,如果是,则将child注册到parent树中去
3.如果向上回溯自己的祖先都不是cancelCtx类型实例,说明整个聊条的取消树都不是连续的,此时只需要监听父类的关闭和自己的取消信号即可

// propagateCancel arranges for child to be canceled when parent is.func propagateCancel(parent Context, child canceler) {    if parent.Done() == nil {        return     }    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]bool)            }            p.children[child] = true        }        p.mu.Unlock()    } else {        go func() {            select {            case <-parent.Done():                child.cancel(false, parent.Err())            case <-child.Done():            }        }()    }}

2.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        }    }}

3.func removeChild(parent Context, child canceler)

如果parent 封装的cancelCtx 字段类型,或者接口里面存放的底层类型是cancelCtx类型,则将其构造树上的节点删除

// removeChild removes a context from its parent.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()}

实际应用

1.测试withDeadline

//测试withDeadlinefunc main(){    root:=context.Background()    son,cancel:=context.WithDeadline(root,time.Now().Add(3*time.Second))    defer cancel()    go work(son,"woker_son")    time.Sleep(100*time.Second)}func work(ctx context.Context,name string){    fmt.Println(name)    label:    for{        select {        case <-ctx.Done():            //到了我们设置的超时时间就会走到这里来            fmt.Println("过了超时时间")            break label        default:            time.Sleep(4*time.Second)            fmt.Println("hahah")        }    }    fmt.Println("跳出循环了")}

运行结果

woker_sonhahah过了超时时间跳出循环了

具体应用场景
其实我们可以利用这个上下文进行一些延时器等应用场景,超时重试等机制可以借助这个来进行而不需要定时器

2.测试withCancel

//测试withDeadlinefunc main(){    root:=context.Background()    son,cancel:=context.WithCancel(root)    fmt.Println(son)    go work(son,"woker_son")    go work(son,"woker_son2")    cancel()    time.Sleep(100*time.Second)}func work(ctx context.Context,name string){    label:    for{        select {        case <-ctx.Done():            //到了我们设置的超时时间就会走到这里来            fmt.Printf("%v 听到了关闭了通道\n",name)            break label        default:            time.Sleep(410*time.Second)            fmt.Println("hahah")        }    }    fmt.Println("跳出循环了")}

结果是:

context.Background.WithCancelwoker_son 听到了关闭了通道跳出循环了woker_son2 听到了关闭了通道跳出循环了

这个应用场景其实相对比较宽泛,timeCtx 和CancerCtx都是做了cancel接口的继承。当父类goroutine退出,就可以通知到其下级。但是具体的多级还是需要各位看客去亲自试一试了,有兴趣可以试一下照着我上篇浅析golang并发的计时器试着用这个写一下,具体应用场景还是挺多的

其他的例子就不多写了,withTimeout实际就是调用的是deadline

一个实际的随机数生成器的例子

func main(){    root:=context.Background()    son,cancel:=context.WithCancel(root)    for i:=0;i<10;i++{    fmt.Println(<-CreateInt(son))    }    cancel()}//  简单的随机数生成器func CreateInt(ctx context.Context)chan int{    ch:=make(chan int)    go func() {        label:            for{                select {                case <-ctx.Done():                    break label                case ch<-rand.Int():                }            }            close(ch)    }()    return ch}

如有问题欢迎讨论,创作不易,转载请标明出处


文章来源:智云一二三科技

文章标题:浅析context

文章地址:https://www.zhihuclub.com/6959.shtml

关于作者: 智云科技

热门文章

网站地图