context

前言

之前浅读过,不过很快就忘记了,再次深入学习下。

本文的是在go version go1.13.15 darwin/amd64进行的

什么是context

go在Go 1.7 标准库引入context,主要用来在goroutine之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。

为什么需要context呢

在并发程序中,由于超时、取消或者一些异常的情况,我们可能要抢占操作或者中断之后的操作。

在Go里,我们不能直接杀死协程,协程的关闭一般会用 channel+select 方式来控制,但是如果某一个操作衍生出了很多的协程,并且相互关联。或者某一个协程层级很深,有很深的子子协程,这时候使用channel+select就比较头疼了。

所以context的适用机制

  • 上层任务取消后,所有的下层任务都会被取消;
  • 中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务。

同时context也可以传值,不过这个很少用到,使用context.Context进行传递参数请求的所有参数一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求ID。

context底层设计

context的实现

type Context interface {
// 返回context被取消的时间
// 当没有设置Deadline时间,返回false
Deadline() (deadline time.Time, ok bool) // 当context被关闭,返回一个被关闭的channel
Done() <-chan struct{} // 在 channel Done 关闭后,返回 context 取消原因
Err() error // 获取key对应的value
Value(key interface{}) interface{}
}

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

Done返回一个只读的channel,类型为struct{},当该context被取消的时候,该channel会被关闭。对于只读的channel,只有被关闭的时候,才能通过select读出对应的零值,字协程监听这个channel,只要能读出值(对应的零值),就可以进行收尾工作了。

Err返回context结束的原因,只有在context被关闭的时候才会返回非空的值。

1、如果context被取消,会返回Canceled错误;

2、如果context超时,会返回DeadlineExceeded错误;

Value获取之前存入key对应的value值。里面的值可以多次拿取。

几种context

emptyCtx

context之源头

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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"
}

emptyCtxdo nothing的方式实现了Context接口。

同时有两个emptyCtx的全局变量

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

通过下面两个导出的函数(首字母大写)对外公开:

func Background() Context {
return background
} func TODO() Context {
return todo
}

这两个我们在使用的时候如何区分呢?

先来看下官方的解释

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests. // TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).

Background 适用于主函数、初始化以及测试中,作为一个顶层的context

TODO适用于不知道传递什么context的情形。

也就是在未考虑清楚是否传递、如何传递context时用TODO,作为发起点的时候用Background

cancelCtx

cancel机制的灵魂

cancelCtx的cancel机制是手工取消、超时取消的内部实现

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement 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
}

看下Done

func (c *cancelCtx) Done() <-chan struct{} {
// 加锁
c.mu.Lock()
// 如果done为空,创建make(chan struct{})
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}

这是个懒汉模式的函数,第一次调用的时候c.done才会被创建。

重点看下cancel

// 关闭 channel,c.done;递归地取消它的所有子节点;从父节点从删除自己。
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
// 关闭channel
// channel没有初始化
if c.done == nil {
// 赋值一个关闭的channel,closedchan
c.done = closedchan
} else {
// 初始化了channel,直接关闭
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)
}
} // 从父节点删除context
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
// 删除child
delete(p.children, child)
}
p.mu.Unlock()
} // closedchan is a reusable closed channel.
var closedchan = make(chan struct{}) func init() {
close(closedchan)
}

这个函数的作用就是关闭channel,递归地取消它的所有子节点;从父节点从删除自己。达到的效果是通过关闭channel,将取消信号传递给了它的所有子节点。

再来看下propagateCancel

// broadcastCancel安排在父级被取消时取消子级。
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
// 找到可以取消的父 context
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
// 父节点取消了
if p.err != nil {
// 取消子节点
child.cancel(false, p.err)
// 父节点没有取消
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// 挂载
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) {
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
}
}
}

这个函数的作用是在parentchild之间同步取消和结束的信号,保证在parent被取消时child也会收到对应的信号,不会出现状态不一致的情况。

上面可以看到,对于指定的几种context是直接cancel方法递归地取消所有的子上下文这可以节省开启新goroutine监听父context是否结束的开销;

对于非指定的也就是自定义的context,运行时会通过启动goroutine来监听父Context是否结束,并在父context结束时取消自己,然而启动新的goroutine是相对昂贵的开销;

对外暴露的WithCancel就是对cancelCtx的应用

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
// 将传入的上下文包装成私有结构体 context.cancelCtx
c := newCancelCtx(parent)
// 构建父子上下文之间的关联
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
} // newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}

使用WithCancel传入一个context,会对这个context进行重新包装。

WithCancel函数返回的CancelFunc被调用或者是父节点的done channel被关闭(父节点的 CancelFunc 被调用),此 context(子节点)的 done channel 也会被关闭。

timerCtx

在来看下timerCtx

type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu. deadline time.Time
} func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 调用context.cancelCtx.cancel
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()
}

cancelCtx的基础之上多了个timerdeadline。它通过停止计时器来实现取消,然后通过cancelCtx.cancel,实现取消。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
// 调用WithDeadline,传入时间
return WithDeadline(parent, time.Now().Add(timeout))
} func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 判断结束时间,是否到了
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 构建timerCtx
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 构建父子上下文之间的关联
propagateCancel(parent, c)
// 计算当前距离 deadline 的时间
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// d 时间后,timer 会自动调用 cancel 函数。自动取消
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}

context.WithDeadline在创建context.timerCtx的过程中判断了父上下文的截止日期与当前日期,并通过time.AfterFunc创建定时器,当时间超过了截止日期后会调用context.timerCtx.cancel同步取消信号。

valueCtx

type valueCtx struct {
Context
key, val interface{}
} 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")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}

如果context.valueCtx中存储的键值对与context.valueCtx.Value方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回nil或者查找到对应的值。

因为查找方向是往上走的,所以,父节点没法获取子节点存储的值,子节点却可以获取父节点的值。

context查找的时候是向上查找,找到离得最近的一个父节点里面挂载的值。

所以context查找的时候会存在覆盖的情况,如果一个处理过程中,有若干个函数和若干个子协程。在不同的地方向里面塞值进去,对于取值可能取到的不是自己放进去的值。

所以在使用context进行传值的时候我们应该慎用,使用context传值是一个比较差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

防止内存泄露

goroutine是很轻量的,但是不合理的使用就会导致goroutine的泄露,也就是内存泄露,具体的内存泄露可参考go中内存泄露的发现与排查

使用context.WithTimeout可以防止内存泄露

func TimeoutCancelContext() {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*800))
go func() {
// 具体的业务逻辑
// 取消超时
defer cancel()
}() select {
case <-ctx.Done():
fmt.Println("time out!!!")
return
}
}

1、通过context的WithTimeout设置一个有效时间为1000毫秒的context。

2、业务逻辑完成会调用cancel(),取消超时,如果在设定的超时时间内,业务阻塞没有完成,就会触发超时的退出。

总结

1、context是并发安全的

2、context可以进行传值,但是在使用context进行传值的时候我们应该慎用,使用context传值是一个比较差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

3、对于context的传值查询,context查找的时候是向上查找,找到离得最近的一个父节点里面挂载的值,所以context在查找的时候会存在覆盖的情况,如果一个处理过程中,有若干个函数和若干个子协程。在不同的地方向里面塞值进去,对于取值可能取到的不是自己放进去的值。

4、当使用 context 作为函数参数时,直接把它放在第一个参数的位置,并且命名为 ctx。另外,不要把 context 嵌套在自定义的类型里。

参考

【Go Context的踩坑经历】https://zhuanlan.zhihu.com/p/34417106

【深度解密Go语言之context】https://www.cnblogs.com/qcrao-2018/p/11007503.html

【深入理解Golang之context】https://juejin.cn/post/6844904070667321357

【上下文 Context】https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/

【Golang Context深入理解】https://juejin.cn/post/6844903555145400334

【《Go专家编程》Go 并发控制context实现原理剖析】https://my.oschina.net/renhc/blog/2249581

go中context源码解读的更多相关文章

  1. go中panic源码解读

    panic源码解读 前言 panic的作用 panic使用场景 看下实现 gopanic gorecover fatalpanic 总结 参考 panic源码解读 前言 本文是在go version ...

  2. etcd中watch源码解读

    etcd中watch的源码解析 前言 client端的代码 Watch newWatcherGrpcStream run newWatchClient serveSubstream server端的代 ...

  3. go中waitGroup源码解读

    waitGroup源码刨铣 前言 WaitGroup实现 noCopy state1 Add Wait 总结 参考 waitGroup源码刨铣 前言 学习下waitGroup的实现 本文是在go ve ...

  4. java中jdbc源码解读

    在jdbc中一个重要的接口类就是java.sql.Driver,其中有一个重要的方法:Connection connect(String url, java.util.Propeties info); ...

  5. go中errgroup源码解读

    errgroup 前言 如何使用 实现原理 WithContext Go Wait 错误的使用 总结 errgroup 前言 来看下errgroup的实现 如何使用 func main() { var ...

  6. Mybatis源码解读-SpringBoot中配置加载和Mapper的生成

    本文mybatis-spring-boot探讨在springboot工程中mybatis相关对象的注册与加载. 建议先了解mybatis在spring中的使用和springboot自动装载机制,再看此 ...

  7. 【原】Spark中Job的提交源码解读

    版权声明:本文为原创文章,未经允许不得转载. Spark程序程序job的运行是通过actions算子触发的,每一个action算子其实是一个runJob方法的运行,详见文章 SparkContex源码 ...

  8. HttpServlet中service方法的源码解读

    前言     最近在看<Head First Servlet & JSP>这本书, 对servlet有了更加深入的理解.今天就来写一篇博客,谈一谈Servlet中一个重要的方法-- ...

  9. AbstractCollection类中的 T[] toArray(T[] a)方法源码解读

    一.源码解读 @SuppressWarnings("unchecked") public <T> T[] toArray(T[] a) { //size为集合的大小 i ...

  10. go 中 sort 如何排序,源码解读

    sort 包源码解读 前言 如何使用 基本数据类型切片的排序 自定义 Less 排序比较器 自定义数据结构的排序 分析下源码 不稳定排序 稳定排序 查找 Interface 总结 参考 sort 包源 ...

随机推荐

  1. Ali266首次商用落地,助力优酷码率最高节省40%

    阿里云自研编码器Ali266于2022年1月在优酷正式上线,这是已知的业界首个H.266/VVC标准的编码器商用落地项目.经过两个月的实际运行数据显示,开启Ali266后,同等画面清晰度的情况下比原H ...

  2. CO01生产订单屏幕增强

    一.生产订单客户屏幕新增字段 二.生产订单抬头AUFK表的CI_AUFK中新增屏幕字段 三.CMOD 增强分配PPCO0012 修改0100屏幕,新增对应字段,其中生产订单类型设置为下拉框 EXIT_ ...

  3. 2024-01-17:lc的30. 串联所有单词的子串

    2024-01-17:用go语言,给定一个字符串 s 和一个字符串数组 words. words 中所有字符串 长度相同. s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接 ...

  4. [计数dp] 整数划分(模板题+计数dp+完全背包变种题)

    计数类 dp 可分为 计数 dp 和数位统计 dp.大多是用来统计方案数什么的,特别强调 不重不漏,在此还是根据各个题的特点将计数 dp 和数位 dp 分开整理.其实数位 dp 的题目会相对多很多- ...

  5. <vue 路由 8、keep-alive的使用>

    一.     知识点 1.什么是keep-alive? keep-alive是Vue.js的一个内置组件. 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们.它自身不会渲染一个 DOM 元素,也 ...

  6. <vue 路由 4、嵌套路由>

    一.效果 点击about后,新闻和体育属于about的子路由调用的页面 知识点说明 路由里使用children属性可以实现路由的嵌套 三.代码结构 注:主要是标红的几个文件 四.代码 重新编写这几个文 ...

  7. 万字血书Vue—Vue的核心概念

    MVVM M:模型(Model):data V:视图(View):模板 VM:视图模型(ViewModel):Vue实例对象 Vue收到了MVVM模型的启发,MVVM是vue实现数据驱动视图和双向数据 ...

  8. Linux telnet安装及端口测试联通性

    安装步骤: 可使用该文中的步骤进行安装,已经过本人验证,是可以安装成功的: https://blog.csdn.net/doubleqinyan/article/details/80492421 安装 ...

  9. [转帖]GB18030 编码

    https://www.qqxiuzi.cn/zh/hanzi-gb18030-bianma.php GB18030编码采用单字节.双字节.四字节分段编码方案,具体码位见下文.GB18030向下兼容G ...

  10. 【转帖】【ethtool】ethtool 网卡诊断、调整工具、网卡性能优化| 解决丢包严重

    目录 即看即用 详细信息 软件简介 安装 ethtool的使用 输出详解 其他指令 将 ethtool 设置永久保存 如何使用 ethtool 优化 Linux 虚拟机网卡性能 ethtool 解决网 ...