并发编程Context 基本用法和如何实现
Posted @了凡
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程Context 基本用法和如何实现相关的知识,希望对你有一定的参考价值。
博主介绍:
– 我是了 凡 微信公众号【了凡银河系】期待你的关注。未来大家一起加油啊~
文章目录
什么是Context?
上下文(Context)在实际开发场景中,在API之间或者方法调用之间,所传递的除了业务参数之外的额外信息。
例如,服务端接收到客户端的HTTP请求之后,可以把客户端的IP地址和端口、客户端的身份信息、请求接收的时间、Trace ID等信息放入到上下文中,这个上下文可以在后端的方法调用中传递,后端的业务方法除了利用正常的参数做一些业务处理(如订单处理)之外,还可以从上下文读取到消息请求的时间、Trace ID等信息,把服务处理的时间推送到Trace服务中。Trace服务可以把同一 Trace ID的不同方法的调用顺序和调用时间展示成流程图,方便跟踪。Go标准库中的Context功能还不止于此,它还提供了超时(Timeout)和取消(Cancel)的机制。
Context 的来历
Go在1.7的版本中才正式把Context加入到标准库中。在这之前,很多Web框架在定义自己的handler时,都会传递一个自定义的Context,把客户端的信息和客户端的请求信息放入到Context中。Go最初提供了golang.org/x/net/context库用来提供上下文信息,最终还是在Go1.7中把此库提升到标准库context包中。
因为在Go1.7之前,很多库都依赖golang.org/x/net/context中的Context实现,这就导致Go1.7发布之后,出现了标准库Context和golang.org/x/net/context并存的状况。新的代码使用标准库Context的时候,没有办法使用这个标准库的Context去调用旧有的使用x/net/context实现的方法。
所以,在Go1.9中,还专门实现了一个叫做type alias的新特性,然后把x/net/context中的Context定义成标准库Context的别名,以解决新旧Context类型冲突问题。例如:
// +build go1.9
package context
import "context"
type Context = context.Context
type CancelFunc = context.CancelFunc
Go标准库的Context不仅提供了上下文传递的信息,还提供了cancel、timeout等其它信息,这些信息貌似和context这个包名没关系,但是还是得到了广泛的应用。所以,你看,context包中的Context不仅仅传递上下文信息,还有timeout等其它功能,有些“名不副实”了。
当然,Context也是有一些问题的,例如:
- Context包名导致使用的时候重复ctx context.Context
- Context.WithValue可以接收任何类型的值,非类型安全;
- Context包名容易误导人,实际上,Context最主要的功能是取消goroutine的执行;
- Context漫天飞,函数污染。
虽然有以上不少问题,但是很多场景下,使用Context还是很方便,很多的Web应用框架,都切换成了标准库的Context。标准库中的database/sql就、os/exec、net、net/http等包中都使用到了Context。而且,如果我们遇到了下面的一些场景,也可以考虑使用Context:
- 上下文信息传递(request-scoped),比如处理http请求、在请求处理链路上传递信息;
- 控制子goroutine的运行;
- 超时控制的方法调用;
- 可以取消的方法调用。
所以,我们需要掌握Context的具体用法,这样才能在不影响主要业务流程实现的时候,实现一些通用的信息传递,或者是能够和其它goroutine协同工作,提供timeout、cancel等机制。
Context基础使用方法
Context接口包含了Deadline、Done、Err和Value这些方法,分别都是干什么用的的?
type Context interface
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct
Err() error
Value(key interface) interface
Deadline 方法
Deadline 方法会返回这个Context被取消的截止日期。如果没有设置截止日期,ok的值是false。后续每次调用这个对象的Deadline方法时,都会返回和第一次调用相同的结果。
Done方法
Done方法返回一个Channel对象。在Context被取消时,此Channel 会被close,如果没被取消,可能会返回nil。后续的Done调用总是返回相同的结果。当Done被close的时候,你可以通过ctx.Err 获取错误信息。Done这个方法名其实起的并不好,因为名字太过笼统,不能明确反映Done被close的原因,因为cancel、timeout、deadline都可能导致Done被close,不过,目前还没有一个更合适的方法名称。
Done方法:如果Done没有被close,Err方法返回nil;如果Done被close,Err方法会返回Done被close的原因。
Value方法
Value返回此ctx中和指定的key相关联的value。
Context中实现了2个常用的生成顶层Context的方法。
- context.Background():返回一个非nil的、空的Context,没有任何值,不会被cancel,不会超时,没有截止日期。一般用在主函数、初始化、测试以及创建根Context的时候。
- context.TODO():返回一个非nil的、空的Context,没有任何值,不会被cancel,不会超时,没有截止日期。当你不清楚是否该用Context,或者目前还不知道要传递一些什么上下文信息的时候,就可以使用这个方法。
对于这两个方法不用考虑,两个底层实现是一摸一样的,可以直接使用context.Background。从我的角度出发来看,Background方法名更符合语义一些。
var
background = new(emptyCtx)
todo = new(emptyCtx)
func Background() Context
return background
func TODO() Context
return todo
Context在使用时,一些默认规则:
- 一般函数使用Context的时候,会把这个参数放在第一个参数的位置。
- 从来不把nil当作Context类型的参数值,可以使用context.Background()创建一个空的上下文对象,也不要使用nil。
- Context只用来临时做函数之间的上下文透传,不能持久化Context或者把Context长久保存。把Context持久化到数据库、本地文件或者全局变量、缓冲中都是错误的用法。(结合实际需求进行考虑)
- key的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用Context时候产生冲突。使用WithValue时,key的类型应该是自己定义的类型。
- 常常使用struct作为底层类型定义key的类型。对于exported key的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。
对于key的类型不要使用string,自己要把握好,如果能保证别人使用你的Context时不会和你定义的key冲突,那么key的类型就比较随意,因为自己保证了不同包的key不会冲突,否则建议尽量采用保守的**unexported(结构体的非导出字段)**的类型。
创建特殊用途Context的方法
4种基本的context类型
- emptyCtx:一个没有任何功能的Context类型,常用做root Context。
- cancelCtx:一个cancelCtx是可以被取消的,同时由它派生出来的Context都会被取消。
- timerCtx:一个timeCtx携带了一个timer(定时器)和截止时间,同时内嵌了一个cancelCtx。当timer到期时,由cancelCtx来实现取消功能。
- valueCtx:一个valueCtx携带了一个key-value对,其它的key-value对由它的parent(父) Context携带。
WithValue
WithValue基于父Context生成一个新的Context,保存了一个key-value键值对。它常常用来传递上下文。
WithValue方法是创建了一个类型为valueCtx的Context,它的类型定义如下:
type valueCtx struct
Context
key, val interface
持有一个key-value键值对,还持有parent Context。覆盖了Value方法,优先从自己的存储中检查这个key,不存在的话会从parent中继续检查。
Go标准库实现的Context还实现了链式查找。如果不存在,还会向parent Context去查找,如果parent还是valueCtx的话,还是遵循相同的原则:valueCtx 会嵌入parent,所以还是会查找parent的Value方法的。
ctx := context.Background()
ctx = context.WithValue(ctx,"key1","0001")
ctx = context.WithValue(ctx,"key2","0002")
ctx = context.WithValue(ctx,"key3","0003")
ctx = context.WithValue(ctx,"key4","0004")
fmt.Println(ctx.Value("key1"))
WithCancel
WithCancel方法返回parent的副本,只是副本中的Done Channel是新建的对象,它的类型是cancelCtx。
常常在一些需要主动取消长时间的任务时,创建这种类型的Context,然后把这个Context传给长时间执行任务的goroutine。当需要终止任务的goroutine。当需要中止任务时,就可以cancel(取消) 这个Context,这样长时间执行任务的goroutine,就可以通过检查这个Context,知道Context已经被取消了。
WithCancel 返回值中的第二个值是一个cancel函数。其实,这个返回值的名称(cancel)和类型(Cancel)也非常迷惑人。
不是只有自己想中途放弃,才去调用cancel,只要自己的任务正常完成了,就需要cancel,这样,这个Context才能释放它的资源(通知它的children处理cancel,从它的parent中把自己移除,甚至释放相关的goroutine)。很多人在使用这个方法的时候,都会忘记调用cancel,一定要记得而且要尽早释放。
WithCancel方法的实现代码:
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
c := newCancelCtx(parent)
propagateCancel(parent, &c) // 把c朝上传播
return &c, func() c.cancel(true, Canceled)
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx
return cancelCtxContext: parent
代码中调用的propagateCancel方法会顺着parent路径往上找,直到找到一个cancelCtx,或者为nil。如果不为空,就把自己加入到这个cancelCtx的child,以便这个cancelCtx被取消的时候通知自己。如果为空,会新起一个goroutine,由它来监听parent的Done是否已关闭。
当这个cancelCtx的cancel函数被调用的时候,或者parent的Done被close的时候,这个cancelCtx的Done才被close。
cancel是向下传递的,如果一个WithCancel生成的Context被cancel时,如果它的子Context(也有肯能是孙,或者更低,依赖子的类型)也是cancelCtx类型的,就会被cancel,但是不会向上传递。parent Context不会因为子Context被cancel而cancel。
cancelCtx被取消时,它的Err字段就是下面这个Canceled错误:
var Canceled = errors.New("context canceled")
WithDeadline
WithDeadline会返回一个parent的副本,并且设置了一个不晚于参数d的截止时间,类型为timerCtx(或者是cancelCtx)。
如果它的截止时间晚于parent的截止时间,那么就以parent的截止时间为准,并返回一个类型为cancelCtx的Context,因为parent的截止时间到了,就会取消这个cancelCtx。
如果当前时间已经超过了截止时间,就直接返回一个已经被cancel的timerCtx。否则就会启动一个定时器,到截止时间取消这个timerCtx。
总结:timerCtx的Done被Close掉,主要的一些事件触发:
- 截止时间到了
- cancel函数被调用
- parent的Done被close
WithDeadline方法源码实现:
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
if cur, ok := parent.Deadline(); ok && cur.Before(d)
// 如果parent的截止时间更早,直接返回一个cancelCtx即可
// The current deadline is already sooner than the new one.
return WithCancel(parent)
c := &timerCtx
cancelCtx: newCancelCtx(parent),
deadline: d,
propagateCancel(parent, c) // 同cancelCtx的处理逻辑
dur := time.Until(d)
if dur <= 0 // 当前时间已经超过了截止时间,直接cancel
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
// 设置一个定时器,到截止时间后取消
c.timer = time.AfterFunc(dur, func()
c.cancel(true, DeadlineExceeded)
)
return c, func() c.cancel(true, Canceled)
WithTimeout
WithTimeout 其实是和 WithDeadline一样,只不过一个参数是超时时间,一个参数是截止时间。超时时间加上当前时间,其实就是截止时间,因此,WithTimeout的源码实现是:
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error)
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
//
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 当前时间+timeout就是deadline
return WithDeadline(parent, time.Now().Add(timeout))
总结
目前Context都用在对于goroutine的控制和传递信息或者超时控制的方法调用,相信本次对于Context已经足够了解了,建议多带入一些模拟需求想象理解。一起加油!
创作不易,点个赞吧!
如果需要后续再看点个收藏!
如果对我的文章有兴趣给个关注!
如果有问题,可以关注公众号【了凡银河系】点击联系我私聊。
以上是关于并发编程Context 基本用法和如何实现的主要内容,如果未能解决你的问题,请参考以下文章