golang context

先翻译一下go官方文档

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries 
and between processes.
Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between 
them must propagate the Context, optionally replacing it with a derived Context created using WithCancel, WithDeadline, WithTimeout, or 
WithValue. When a Context is canceled, all Contexts derived from it are also canceled.

The WithCancel, WithDeadline, and WithTimeout functions take a Context (the parent) and return a derived Context (the child) and a CancelFunc. 
Calling the CancelFunc cancels the child and its children, removes the parent's reference to the child, and stops any associated timers. Failing to
 
call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires. The go vet tool checks that CancelFuncs are used 
on all control-flow paths.

Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check 
context propagation:

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, 
typically named ctx: 


Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.

Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines. 

翻译

package context定义了context类型,该类型在API边界之间以及进程之间传递截止日期,取消信号和其他请求范围的值。

1.对服务器的传入"请求应创建一个上下文",而对服务器的传出调用应接受一个上下文。它们之间的函数
调用链必须传播Context,可以选择将其替换为使用WithCancel,WithDeadline,WithTimeout 
,WithValue创建的派生Context。取消上下文后,从该上下文派生的所有上下文也会被取消。

2.WithCancel,WithDeadline和WithTimeout函数采用Context(父级)并返回派生的Context(子级)和
CancelFunc。调用CancelFunc会取消该子代及其子代,删除父代对该子代的引用,并停止所有关联的计
时器。"未能调用CancelFunc会使子代及其子代泄漏",直到父代被取消或计时器触发。go vet 检查有
控制流路径上是否都使用了CancelFuncs。

3.使用上下文的程序应遵循以下规则,以使各个包之间的接口保持一致,并使静态分析工具可以检查上下
文传播:

4.不要将上下文存储在结构类型中;而是将上下文明确传递给需要它的每个函数。 Context应该是第一个参数,通常命名为ctx:

5.即使函数允许,也不要传递nil Context。如果不确定使用哪个上下文,请传递context.TODO。

6.仅将上下文值用于传递流程和API的请求范围的数据,而不用于将可选参数传递给函数。

7.可以将相同的上下文传递给在不同goroutine中运行的函数。上下文对于由多个goroutine同时使用是安全的
func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}

总结一下:

  • 第一点:所有的请求都要保存一个context,被调用的方法应该也要一个context,虽然会污染所有的方法,但是这个是对流程控制和级联取消的一个关键
  • 第二点:一定不能忘记defer cancer() 取消方法,不然那容易导致其及其子代泄露,使用go vet 可以检测出来
  • 第三点:所有方法都应该保持第一个参数是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 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"
}

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()context
2.  func Todo()context
3.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

//测试withDeadline
func 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_son
hahah
过了超时时间
跳出循环了

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

2.测试withCancel

//测试withDeadline
func 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.WithCancel
woker_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
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355