context源码

Context接口有四个方法,

Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。

Done方法返回一个只读的chan(我感觉不是只读可以写入零值),类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求(即已经调用了cancel函数),我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。

Err方法返回取消的错误原因,因为什么Context被取消。

Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的

1 WithCancel   要想控制一个goroutine结束,可以通过cancel()来控制子context来结束该goroutine,WithCancel是context包下的一个函数,不是一个方法,可以直接执行,如果开启的go协程没有执行cancel函数来结束,就会发生内存泄漏

func main() {
    // WithCancel返回一个子context和一个可以终止该context的函数cancel,
    // context.Background()是一个空的Context,用于整个Context的根节点,
    ctx, cancel := context.WithCancel(context.Background())
    go func(ctx context.Context) {
        for {
            select {
            // ctx实现了Context接口,在下面调用cancel函数的时候,有close(c.done)关闭了Done中生成的chan,
            // 任何chan关闭后读取它时,都会返回里面类型的零值,这样Done中的chan就起到了关闭goroutine的作用,
            case <-ctx.Done():
                fmt.Println("监控退出,停止了...")
                return
            default:
                fmt.Println("goroutine监控中...")
                time.Sleep(2 * time.Second)
            }
        }
    }(ctx)
    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}
View Code

WithCancel源码,

// cancelCtx可以被取消,如果取消,则它的所有child也都被取消,
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 WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    // 将传入的上下文包装成私有结构体 context.cancelCtx
    c := newCancelCtx(parent)
    // 会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消
    // 作用是在 parent 和 child 之间同步取消和结束的信号,
    // 保证在 parent 被取消时,child 也会收到对应的信号,不会发生状态不一致的问题
    // 核心是里面 p.children[child] = struct{}{} 的注册,
    propagateCancel(parent, &c)
    // 返回构建的子context的地址和匿名函数,
    // 新创建一个可取消的 context 节点,返回的 cancelFunc 函数会传入 true。这样做的结果是:
    // 当调用返回的 cancelFunc 时,会将这个 context 从它的父节点里“除名”,因为父节点可能有很多子节点,
    // 你自己取消了,所以我要和你断绝关系,对其他人没影响。
    return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
// 返回一个私有的cancelCtx,
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

// propagateCancel arranges for child to be canceled when parent is.
// propagateCancel当父上下文取消时,它所有子上下文也会被取消,
// https://blog.csdn.net/weixin_42416759/article/details/108460970
func propagateCancel(parent Context, child canceler) {
    // 取出父上下文的chan,
    done := parent.Done()
    // 若为空,说明是emptyCtx,不会被取消,
    if done == nil {
        return // parent is never canceled
    }
    select {
    case <-done:
        // // 若父上下文已经取消了,则调用cancel将当前子上下文及其所有的child都取消,
        child.cancel(false, parent.Err())
        return
    default:
    }
    // 确定parent最内层的cancel内部是否实现了cancelCtx,因为cancelCtx实现了cancel接口,cancel里面
    // 里面有cancel方法,之后会通过调用该方法来结束goroutine,
    // 如果是,则把child放入该cancelCtx的child中
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)
        } else {
            // 将子上下文放入父上下文的children中,这样如果父上下文取消,则子上下文也会取消,
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        // 如果不是内部实现的cancelCtx则另外起一个协程
        // 一直监听父context是否关闭,如果关闭则关闭child
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}
// https://studygolang.com/articles/16320?fr=sidebar 之前的版本是用switch匹配类型来实现的,
// 这个初始化了一个标记cancelCtxKey实现更加优雅,
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    done := parent.Done()
    if done == closedchan || done == nil {
        return nil, false
    }
    // 判断parent是否封装了cancelctx,如果是,则判断是否重写了Done方法,否则直接返回nil false,
    // 这里的Value方法是不断调用Value实现的,
    p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
    if !ok {
        return nil, false
    }
    // 检查parent(即candelCtx)的done是否被重写了,如果ok为true,说明没有重写,返回true,
    // 将child加入parent的children列表中,等待parent释放取消信号
    // 如果ok为false,则重写了Done,需要另外判断,
    p.mu.Lock()
    ok = p.done == done
    p.mu.Unlock()
    if !ok {
        return nil, false
    }
    return p, true
}

// 如果parent里封装了cancelCtx,则最后必定会调用这个返回c,因为传入的key就是cancelCtxKey,
// cancelCtxKey只有在这里会用到,如果传入的key是cancelCtxKey,则走到这里才会返回,也就是说
// 一旦传入的是cancelCtxKey,最后返回的一定是cancelCtx,
func (c *cancelCtx) Value(key interface{}) interface{} {
    if key == &cancelCtxKey {
        return c
    }
    return c.Context.Value(key)
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel函数的三个作用
// 1 关闭当前goroutine的chan,
// 2 遍历当前goroutine的children,递归关闭它的所有子上下文
// 3 removeFromParent为true时,将当前子上下文和它的父上下文断绝关系,
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()
  // removeFormParent只有第一次才为true,之后就没有必要了,为true的意思是当前的子上下文要和它的父上下文断绝关系
  // 即删除父上下文children中的子上下文,之后就为false了,没必要再删除了,因为当前子上下文中children中所有goroutine
  // 都会被删除,所以上面的child.cancel(false,err) 中传的是false,
    if removeFromParent {
        // 这里的c就是WithCancel中生成的c := newCancelCtx(parent),c.Context是它的父上下文,c是子上下文,
        // 这个函数就是要把父上下文中children中的子上下文删除 delete(p.children, child),
        // 这个是之前用propagateCancel(parent, &c)来注册的,
        removeChild(c.Context, c)
    }
}
View Code

2  WithDeadline 会多传递一个截止时间参数,意味着到了这个时间点(是绝对时间),会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。 

WithTimeout 表示是超时自动取消,是多少时间后自动取消Context的意思,(是相对时间,特别注意这里关闭有两种方法,一种是用cancel()去主动结束goroutine(如果goroutine运行结束了,不手动cancel的话,它会等待timeout的时间,才会释放内存),另一种是等超过所设定的timeout time.Duration后,内部会自动关闭goroutine,这两种情况都会使ctx.Done()返回close的channel,也即都可通过ctx.Done()来监控,哪个先执行都会导致goroutine结束,之所以不在goroutine跑完后自动结束,是因为把cancel的控制权交给了程序员,如果直接结束的话,这个goroutine下所有的goroutine都会结束,

3 利用WithValue方法可以给context附加值,

package main
import (
    "context"
    "fmt"
    "time"
)
var key string="name"
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    //附加值,将k-v对加到了context中,
    valueCtx:=context.WithValue(ctx,key,"【监控1】")
    go watch(valueCtx)
    time.Sleep(10 * time.Second)
    fmt.Println("可以了,通知监控停止")
    cancel()
    //为了检测监控过是否停止,如果没有监控输出,就表示停止了
    time.Sleep(5 * time.Second)
}
func watch(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            //取出值,Value是Context接口的方法,
            fmt.Println(ctx.Value(key),"监控退出,停止了...")
            return
        default:
            //取出值
            fmt.Println(ctx.Value(key),"goroutine监控中...")
            time.Sleep(2 * time.Second)
        }
    }
}
View Code

参考: https://www.flysnow.org/2017/05/12/go-in-action-go-context.html

https://zhuanlan.zhihu.com/p/68792989

context.Context的作用就是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期

1 对与context的取消功能主要是

1> 用ctx.Done()监听,2> 用cancel()主动取消,3> 基于时间取消

 https://segmentfault.com/a/1190000022484275

 

 

 

官方案例:https://deepzz.com/post/golang-context-package-notes.html

 context学习:https://www.linkinstar.wiki/2019/06/27/golang/source-code/context-code-view/

 

 

 

 

 

 

 

 

1 struct{}类型的chan只能接受struct{}{},其余的都不行,

package main
import (
    "fmt"
    "time"
)
func fun1(m chan int) {
    time.Sleep(5*time.Second)
    m <- 4
}
func fun2(m chan struct{})  {
    m <- struct{}{}
}
func main()  {
    type s struct {
        name string
    }
    c1 := make(chan int)
    c2 := make(chan struct{})
    //k := s{name: "tom"}
    // 由于c2是struc{}类型的channel,好像只能写入struct{}{},其它类型无法写入
    // https://www.jianshu.com/p/7f45d7989f3a
    //c2 <- struct{}{}
    go fun1(c1)
    go fun2(c2)
    select {
    case p := <- c1:
        fmt.Println("c1接受到了值", p, c1)
    case q := <- c2:
        fmt.Println("c2接收到了值", q, c2)
    }
}
View Code

Done()的用法就是在context对象结束时返回一个channel,瞎写的,

package main
import (
    "context"
    "fmt"
    "time"
)
func main() {
    // 创建一个子节点的context,3秒后自动超时
    // WithTimeout()函数接受一个 Context 和超时时间作为参数,即context的过期时间,
    // 返回其子Context和取消函数cancel
    ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
    go watch(ctx, "监控1")
    go watch(ctx, "监控2")
    fmt.Println("现在开始等待8秒,time=", time.Now().Unix())
    time.Sleep(8 * time.Second)
    fmt.Println("等待8秒结束,准备调用cancel()函数,发现两个子协程已经结束了,time=", time.Now().Unix())
    // 若不调用cancel函数,到了原先创建Contetx时的超时时间,它也会自动调用cancel()函数,
    // 即会往子Context的Done通道发送消息
    cancel()
}
// 单独的监控协程
func watch(ctx context.Context, name string) {
    for {
        select {
        // Done()函数 当ctx返回一个channel,若该channel为空,则
        case k := <-ctx.Done():
            fmt.Println("-----", k)
            fmt.Println(name, "收到信号,监控退出,time=", time.Now().Unix())
            return
        default:
            fmt.Println(name, "goroutine监控中,time=", time.Now().Unix())
            time.Sleep(1 * time.Second)
        }
    }
}
// 特别注意Done()返回的类型是struct{}类型的chan,这种chan只能在接收struct{}{}或close(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
//}
View Code

 

 

 

https://www.yuque.com/baxiang/go/oy5l3k

https://zhuanlan.zhihu.com/p/165621566

https://blog.csdn.net/ruanhao1203/article/details/80044026

https://studygolang.com/articles/17796

https://zhuanlan.zhihu.com/p/174532117

http://interview.wzcu.com/Golang/%E5%9F%BA%E7%A1%80.html

https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/

https://zhuanlan.zhihu.com/p/91559562

https://zhuanlan.zhihu.com/p/68792989

posted on 2020-12-27 20:34  吃我一枪  阅读(125)  评论(0编辑  收藏  举报

导航