剖析Golang Context:从使用场景到源码分析

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"因为goroutine,go的并发非常方便,但是这也带来了另外一个问题,当我们进行一个耗时的异步操作时,如何在约定的时间内终止该操作并返回一个自定义的结果?这也是大家常说的我们如何去终止一个goroutine(因为goroutine不同于os线程,没有主动interrupt机制),这里就轮到今天的主角context登场了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context源于google,于1.7版本加入标准库,按照官方文档的说法,它是一个请求的全局上下文,携带了截止时间、手动取消等信号,幷包含一个并发安全的map用于携带数据。context的API比较简单,标准库实现上也比较干净、独立,接下来我会从具体的使用场景和源码分析两个角度进行阐述。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"使用技巧"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用场景一: 请求链路传值"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般来说,我们的根context会在请求的入口处构造如下"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"ctx := context.Background()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果拿捏不准是否需要一个全局的context,可以使用下面这个函数构造"}]},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"ctx := context.TODO()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"但是不可以为nil"},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"传值使用方式如下"}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n)\n\nfunc func1(ctx context.Context) {\n\tctx = context.WithValue(ctx, \"k1\", \"v1\")\n\tfunc2(ctx)\n}\nfunc func2(ctx context.Context) {\n\tfmt.Println(ctx.Value(\"k1\").(string))\n}\n\nfunc main() {\n\tctx := context.Background()\n\tfunc1(ctx)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们在func1通过WithValue(parent Context, key, val interface{}) Context,赋值k1为v1,在其下层函数func2通过ctx.Value(key interface{}) interface{}获取k1的值,比较简单。这里有个疑问,如果我是在func2里赋值,在func1里面能够拿到这个值吗?答案是不能,context只能自上而下携带值,这个是要注意的一点。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用场景二: 取消耗时操作,及时释放资源"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以考虑这样一个问题,如果没有context包,我们如何取消一个耗时操作呢?我这里模拟了两种写法:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"网络交互场景,经常通过SetReadDeadline、SetWriteDeadline、SetDeadline进行超时取消"}]}]}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"timeout := 10 * time.Second\nt = time.Now().Add(timeout)\nconn.SetDeadline(t)"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"耗时操作场景,通过select模拟"}]}]}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"package main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n)\n\nfunc func1() error {\n\trespC := make(chan int)\n\t// 处理逻辑\n\tgo func() {\n\t\ttime.Sleep(time.Second * 3)\n\t\trespC child映射关系(cancelCtx、timerCtx这两种类型会建立,valueCtx类型会一直向上寻找,而循环往上找是因为cancel是必须的,然后找一种最合理的),这里children的key是canceler接口,并不能处理所有的外部类型,所以会有else,示例见上述代码注释处。对于其他外部类型,不建立直接的传递关系。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"parentCancelCtx定义如下:"}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"func parentCancelCtx(parent Context) (*cancelCtx, bool) {\n\tfor {\n\t\tswitch c := parent.(type) {\n\t\tcase *cancelCtx:\n\t\t\treturn c, true\n\t\tcase *timerCtx:\n\t\t\treturn &c.cancelCtx, true\n\t\tcase *valueCtx:\n\t\t\tparent = c.Context // 循环往上寻找\n\t\tdefault:\n\t\t\treturn nil, false\n\t\t}\n\t}\n}"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"context是如何触发取消的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上文在阐述传递性时的实现时,也包含了一部分取消机制的代码,这里不会再列出源码,但是会依据上述源码进行说明。对于几种context,传递过程大同小异,但是取消机制有所不同,针对每种类型,我会一一解释。不同类型的context可以在一条链路进行取消,但是每一个context的取消只会被一种条件触发,所以下面会单独介绍下每一种context的取消机制(组合取消的场景,按照先到先得的原则,无论那种条件触发的,都会传递调用cancel)。这里有两个设计很关键:"}]},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":" cancel函数是幂等的,可以被多次调用。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":" context中包含done channel可以用来确认是否取消、通知取消。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "},{"type":"text","marks":[{"type":"strong"}],"text":"cancelCtx类型"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"cancelCtx会主动进行取消,在自顶向下取消的过程中,会遍历children context,然后依次主动取消。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"cancel函数定义如下"}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"func (c *cancelCtx) cancel(removeFromParent bool, err error) {\n\tif err == nil {\n\t\tpanic(\"context: internal error: missing cancel error\")\n\t}\n\tc.mu.Lock()\n\tif c.err != nil {\n\t\tc.mu.Unlock()\n\t\treturn // already canceled\n\t}\n\tc.err = err\n\tif c.done == nil {\n\t\tc.done = closedchan\n\t} else {\n\t\tclose(c.done)\n\t}\n\tfor child := range c.children {\n\t\t// NOTE: acquiring the child's lock while holding parent's lock.\n\t\tchild.cancel(false, err)\n\t}\n\tc.children = nil\n\tc.mu.Unlock()\n\n\tif removeFromParent {\n\t\tremoveChild(c.Context, c)\n\t}\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"timerCtx类型"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"WithTimeout是通过WithDeadline来实现的,均对应timerCtx类型。通过parentCancelCtx函数的定义我们知道,timerCtx也会记录父子context关系。但是timerCtx是通过timer定时器触发cancel调用的,部分实现如下"}]},{"type":"codeblock","attrs":{"lang":"golang"},"content":[{"type":"text","text":"\tif c.err == nil {\n\t c.timer = time.AfterFunc(dur, func() {\n\t c.cancel(true, DeadlineExceeded)\n })\n\t}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"静默包含context"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里暂时只想到了静默包含即type A struct{context.Context}的情况。通过parentCancelCtx和propagateCancel我们知道这种context不会建立父子context的直接联系,但是会通过单独的goroutine去检测done channel,来确定是否需要触发链路上的cancel函数,实现见propagateCancel的else部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"结尾"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context的使用注意大致有以下三点:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context只能自顶向下传值,反之则不可以。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果有cancel,一定要保证调用,否则会造成资源泄露,比如timer泄露。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context一定不能为nil,如果不确定,可以使用context.TODO()生成一个empty的context。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"context的实现并不复杂,但在实际使用中能给大家带来不小的便利。使用场景部分通过介绍几个常见的开发场景使大家对号入座,熟练地使用context;源码分析部分,通过了解context的实现,能够在context使用中更加得心应手,做到知其然知其所以然。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"参考资料"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://golang.org/pkg/context","title":""},"content":[{"type":"text","text":"golang官方包"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://blog.golang.org/context","title":""},"content":[{"type":"text","text":"Go Concurrency Patterns: Context"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https://github.com/etcd-io/etcd/blob/master/client/client.go#L543","title":""},"content":[{"type":"text","text":"etcd客户端超时处理示例代码"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章