最近实现系统的分布式日志与事务管理时,在寻求所谓的全局惟一Goroutine ID无果以后,决定仍是简单利用Context机制实现了基本的想法,不够高明,可是好用。因而对它当初的设计比较好奇,便有了此文。
Context是Golang官方定义的一个package,它定义了Context类型,里面包含了Deadline/Done/Err方法以及绑定到Context上的成员变量值Value,具体定义以下:git
type Context interface { // 返回Context的超时时间(超时返回场景) Deadline() (deadline time.Time, ok bool) // 在Context超时或取消时(即结束了)返回一个关闭的channel // 即若是当前Context超时或取消时,Done方法会返回一个channel,而后其余地方就能够经过判断Done方法是否有返回(channel),若是有则说明Context已结束 // 故其能够做为广播通知其余相关方本Context已结束,请作相关处理。 Done() <-chan struct{} // 返回Context取消的缘由 Err() error // 返回Context相关数据 Value(key interface{}) interface{} }
那么到底什么Context?github
能够字面意思能够理解为上下文,比较熟悉的有进程/线程上线文,关于Golang中的上下文,一句话归纳就是:goroutine的相关环境快照,其中包含函数调用以及涉及的相关的变量值。
经过Context能够区分不一样的goroutine请求,由于在Golang Severs中,每一个请求都是在单个goroutine中完成的。golang
注:关于goroutine的理解能够移步这里。数据库
因为在Golang severs中,每一个request都是在单个goroutine中完成,而且在单个goroutine(不妨称之为A)中也会有请求其余服务(启动另外一个goroutine(称之为B)去完成)的场景,这就会涉及多个Goroutine之间的调用。若是某一时刻请求其余服务被取消或者超时,则做为深陷其中的当前goroutine B须要当即退出,而后系统才可回收B所占用的资源。
即一个request中一般包含多个goroutine,这些goroutine之间一般会有交互。分布式
那么,如何有效管理这些goroutine成为一个问题(主要是退出通知和元数据传递问题),Google的解决方法是Context机制,相互调用的goroutine之间经过传递context变量保持关联,这样在不用暴露各goroutine内部实现细节的前提下,有效地控制各goroutine的运行。函数
如此一来,经过传递Context就能够追踪goroutine调用树,并在这些调用树之间传递通知和元数据。
虽然goroutine之间是平行的,没有继承关系,可是Context设计成是包含父子关系的,这样能够更好的描述goroutine调用之间的树型关系。post
生成一个Context主要有两类方法:google
要建立Context树,首先就是要建立根节点spa
// 返回一个空的Context,它做为全部由此继承Context的根节点 func Background() Context
该Context一般由接收request的第一个goroutine建立,它不能被取消、没有值、也没有过时时间,常做为处理request的顶层context存在。.net
有了根节点以后,接下来就是建立子孙节点。为了能够很好的控制子孙节点,Context包提供的建立方法均是带有第二返回值(CancelFunc类型),它至关于一个Hook,在子goroutine执行过程当中,能够经过触发Hook来达到控制子goroutine的目的(一般是取消,即让其停下来)。再配合Context提供的Done方法,子goroutine能够检查自身是否被父级节点Cancel:
select { case <-ctx.Done(): // do some clean… }
注:父节点Context能够主动经过调用cancel方法取消子节点Context,而子节点Context只能被动等待。同时父节点Context自身一旦被取消(如其上级节点Cancel),其下的全部子节点Context均会自动被取消。
有三种建立方法:
// 带cancel返回值的Context,一旦cancel被调用,即取消该建立的context func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // 带有效期cancel返回值的Context,即必须到达指定时间点调用的cancel方法才会被执行 func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) // 带超时时间cancel返回值的Context,相似Deadline,前者是时间点,后者为时间间隔 // 至关于WithDeadline(parent, time.Now().Add(timeout)). func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
下面来看改编自Advanced Go Concurrency Patterns视频提供的一个简单例子:
package main import ( "context" "fmt" "time" ) func someHandler() { // 建立继承Background的子节点Context ctx, cancel := context.WithCancel(context.Background()) go doSth(ctx) //模拟程序运行 - Sleep 5秒 time.Sleep(5 * time.Second) cancel() } //每1秒work一下,同时会判断ctx是否被取消,若是是就退出 func doSth(ctx context.Context) { var i = 1 for { time.Sleep(1 * time.Second) select { case <-ctx.Done(): fmt.Println("done") return default: fmt.Printf("work %d seconds: \n", i) } i++ } } func main() { fmt.Println("start...") someHandler() fmt.Println("end.") }
输出结果:
注意,此时doSth方法中case之done的fmt.Println("done")
并无被打印出来。
超时场景:
package main import ( "context" "fmt" "time" ) func timeoutHandler() { // 建立继承Background的子节点Context ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) go doSth(ctx) //模拟程序运行 - Sleep 10秒 time.Sleep(10 * time.Second) cancel() // 3秒后将提早取消 doSth goroutine } //每1秒work一下,同时会判断ctx是否被取消,若是是就退出 func doSth(ctx context.Context) { var i = 1 for { time.Sleep(1 * time.Second) select { case <-ctx.Done(): fmt.Println("done") return default: fmt.Printf("work %d seconds: \n", i) } i++ } } func main() { fmt.Println("start...") timeoutHandler() fmt.Println("end.") }
输出结果:
前面铺地了这么多。
确实,经过引入Context包,一个request范围内全部goroutine运行时的取消能够获得有效的控制。可是这种解决方式却不够优雅。
一旦代码中某处用到了Context,传递Context变量(一般做为函数的第一个参数)会像病毒同样蔓延在各处调用它的地方。好比在一个request中实现数据库事务或者分布式日志记录,建立的context,会做为参数传递到任何有数据库操做或日志记录需求的函数代码处。即每个相关函数都必须增长一个context.Context类型的参数,且做为第一个参数,这对无关代码彻底是侵入式的。
更多详细内容可参见:Michal Strba 的context-should-go-away-go2文章
Google Group上的讨论可移步这里。
Context机制最核心的功能是在goroutine之间传递cancel信号,可是它的实现是不彻底的。
Cancel能够细分为主动与被动两种,经过传递context参数,让调用goroutine能够主动cancel被调用goroutine。可是如何得知被调用goroutine何时执行完毕,这部分Context机制是没有实现的。而现实中的确又有一些这样的场景,好比一个组装数据的goroutine必须等待其余goroutine完成才可开始执行,这是context明显不够用了,必须借助sync.WaitGroup。
func serve(l net.Listener) error { var wg sync.WaitGroup var conn net.Conn var err error for { conn, err = l.Accept() if err != nil { break } wg.Add(1) go func(c net.Conn) { defer wg.Done() handle(c) }(conn) } wg.Wait() return err }
https://golang.org/pkg/context/
https://faiface.github.io/pos...
https://juejin.im/entry/58088...
https://dave.cheney.net/2017/...
https://dave.cheney.net/2017/...
https://sites.google.com/site...