2020-04-17

golang的context

一个简单的并发

举个例子

什么是context

我理解的context类似于一个保存了状态的object,他被翻译为上下文,但其实它更像环境状态,记载了当前情况下程序执行状态,也就是上文的状态,会影响下文的发展。
每个Goroutine在执行之前,都要先知道“程序当前的执行状态”。通常,将这些执行状态封装在一个Context变量中,传递到要执行的Goroutine中。当状态发生了变化,和这个状态进行过交互的一个或者多个Goroutine也会发生相应的变化。

或者,再简单一点,代码c = a + b
a,b的value就是context。

结束一个goroutine

我们都知道一个goroutine启动后,我们是无法控制他的,大部分情况是等待它自己结束,那么如果这个goroutine是一个不会自己结束的后台goroutine呢?比如监控等,会一直运行的。

这种情况化,一直傻瓜式的办法是全局变量,其他地方通过修改这个变量完成结束通知,然后后台goroutine不停的检查这个变量,如果发现被通知关闭了,就自我结束。

这种方式也可以,但是首先我们要保证这个变量在多线程下的安全,基于此,有一种更好的方式:chan + select 。

这是一个chan+select来结束一个goroutine的方式

虽然可以用channel+select来从外部杀死某个线程,但是某些情况下会比较麻烦,例如由一个请求衍生出多个线程并且之间需要满足一定的约束关系,以实现一些诸如:有效期,中止线程树,传递请求全局变量之类的功能。这就是context的优势。

context重写

每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

刚刚是控制一个goroutine,现在控制三个

golang内置的context包

context包可以提供一个请求从API请求边界到各goroutine的请求域数据传递、取消信号及截至时间等能力。

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

WithCancel,WithDeadline和WithTimeout函数采用Context(父级)并返回派生的Context(子级)和CancelFunc。调用CancelFunc会取消该子代及其子代,删除父代对该子代的引用,并停止所有关联的计时器。未能调用CancelFunc会使子代及其子代泄漏,直到父代被取消或计时器触发。

Goroutine的创建和调用关系是分层级的。更靠顶部的Goroutine应有办法主动关闭其下属的Goroutine的执行(否则,程序就可能失控)。为了实现这种关系,Context结构像一棵树,叶子节点须总是由根节点衍生出来的。

所有的context的父对象,也叫根对象,根节点,是一个空的context,它不能被取消,它没有值,从不会被取消,也没有超时时间,它常常作为处理request的顶层context存在,然后通过WithCancel、WithTimeout函数来创建子对象来获得cancel、timeout的能力.这也就是后面提到的emptyCtx。

context.Context

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
字段 含义
Deadline 返回一个time.Time,表示当前Context应该结束的时间,ok则表示有结束时间
Done 当Context被取消或者超时时候返回的一个close的channel,告诉给context相关的函数要停止当前工作然后返回了。(这个有点像全局广播)
Err context被取消的原因
Value context实现共享数据存储的地方,是协程安全的

以上常用的就是Done,如果Context取消的时候,我们就可以得到一个关闭的chan,关闭的chan是可以读取的,所以只要可以读取的时候,就意味着收到Context取消的信号了,以下是这个方法的经典用法。

  func Stream(ctx context.Context, out chan<- Value) error {
    for {
        v, err := DoSomething(ctx)
        if err != nil {
            return err
        }
        select {
        case <-ctx.Done():
            return ctx.Err()
        case out <- v:
        }
    }
  }

基本数据结构

context的创建者称为root节点,其一般是一个处理上下文的独立goroutine。root节点负责创建Context的具体对象,并将其传递到其下游调用的goroutine. 下游的goroutine可以继续封装改Context对象,再传递更下游的goroutine.这些下游goroutine的Context 对象实例都要逐层向上注册。这样通过root节点的Context对象就可以遍历整个Context对象树,所以通知也能通知到下游的goroutine.

4种context

image.png

Context接口并不需要我们实现,Go内置已经帮我们实现了2个,我们代码中最开始都是以这两个内置的作为最顶层的partent context,衍生出更多的子Context。

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

这两个私有变量都是通过 new(emptyCtx) 语句初始化的,它们是指向私有结构体 context.emptyCtx的指针,这是最简单、最常用的上下文类型:

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
}

从源代码来看,[context.Background] 和 [context.TODO] 函数其实也只是互为别名,没有太大的差别。它们只是在使用和语义上稍有不同:

  • [context.Background]是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来;
  • [context.TODO]应该只在不确定应该使用哪种上下文时使用;

在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 [context.Background]作为起始的上下文向下传递。

继承衍生

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

这四个With函数,接收的都有一个partent参数,就是父Context,我们要基于这个父Context创建出子Context的意思,这种方式可以理解为子Context对父Context的继承,也可以理解为基于父Context的衍生。

通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。而这四个,就是用来创建它的子节点、孙节点。

WithCancel函数,传递一个父Context作为参数,返回子Context,以及一个取消函数用来取消Context。
WithDeadline函数,和WithCancel差不多,它会多传递一个截止时间参数,意味着到了这个时间点,会自动取消Context,当然我们也可以不等到这个时候,可以提前通过取消函数进行取消。
WithTimeout和WithDeadline基本上一样,这个表示是超时自动取消,是多少时间后自动取消Context的意思。
WithValue函数和取消Context无关,它是为了生成一个绑定了一个键值对数据的Context

context.Withcancel

context.WithCancel函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数(CancelFunc)。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。

image.png

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

这个代码的实现是一个套娃!!!我放个链接,有空的可以看看,戳这

context.Withvalue

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.WithValue方法附加一对K-V的键值对,这里Key必须是等价性的,也就是具有可比性;Value值要是线程安全的。

这样我们就生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value方法读取ctx.Value(key)。

记住,使用WithValue传值,一般是必须的值,不要什么值都传递。

image.png

原理

Context 的调用应该是链式的,通过WithCancel,WithDeadline,WithTimeout或WithValue派生出新的 Context。当父 Context 被取消时,其派生的所有 Context 都将取消。

通过4个context.WithXXX都将返回新的 Context 和 CancelFunc。调用 CancelFunc 将取消子代,移除父代对子代的引用,并且停止所有定时器。未能调用 CancelFunc 将泄漏子代,直到父代被取消或定时器触发。go vet工具检查所有流程控制路径上使用 CancelFuncs。

小结

1.不要把Context放在结构体中,要以参数的方式传递
2.以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
3.给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
4.Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
5.Context是线程安全的,可以放心的在多个goroutine中传递
6.为了保证父Context对象的创建环境获得对子Context将要被传递到的Goroutine的撤销权,当通过父Context对象创建子Context对象时,可同时获得子Context的一个撤销函数。

  • 为什么不应该放在结构体?

Context 最基本的作用,是对一些 不那么全局的全局变量 的打包,把它放到结构体,其生存周期和作用域是无法控制的,相当于把它变成了它所在包的一个全局变量,那和最开始的目的不久矛盾了吗。

  • 为什么 HTTP 包的 Request 结构体持有 context?

Request 本身就是一堆参数的集合,只不过参数太多单独写成结构体了而已,这堆参数在请求结束时或者读写超时时,就应该释放,需要一个可超时的 Context 来协助。那为什么不把请求参数都放在 Context 呢,因为可读性是非常重要的。

  • 为什么是并发安全的?

Context 本身的实现是不可变的,既然不可变,那当然是线程安全的。并且通过 Context.Done() 返回的通道可以协调 goroutine 的行为。

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

推荐阅读更多精彩内容