前言
大家应该会经常看到这样的代码:很多方法第一个参数都是context,而且还一路往下透传......
本文章的目标就是学习了解context包的作用、应用场景、最佳实践、原理&源码。
简介-是什么、有什么用
context包是go 1.7开始引入到go标准库的,是专门用来简化对于处理单个请求的多个goroutine之间与 请求域的 数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
作用:
- 在goroutine之间传递、共享上下文信息
- 在goroutine之间传递取消信号,用于控制goroutine执行,比如:取消goroutine执行、超时取消goroutine执行
-
在一个goroutine的方法调用链路上传递、共享上下文信息(这个作用类似Java的ThreadLocal)
目前我们常用的一些库都支持context,例如gin、database/sql等库都是支持context的,这样更方便我们做并发控制了,只要在请求处理入口创建一个context上下文,不断透传下去即可。
demo
WithValue
使用context包的
func WithValue(parent Context, key, val any) Context,会返回valueCtx实例作用:设置键值对,共享数据
const LogIDKey = "LogID"
const CallerKey = "caller"
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, LogIDKey, "123456")
go test1(ctx)
go test2(ctx)
for {
}
}
func test1(ctx context.Context) {
fmt.Println("test1 run... logId:", ctx.Value(LogIDKey))
ctx = context.WithValue(ctx, CallerKey, "test1")
test111(ctx)
}
func test111(ctx context.Context) {
fmt.Printf("test111 run... caller: %s, logId:%s\n", ctx.Value(CallerKey), ctx.Value(LogIDKey))
}
func test2(ctx context.Context) {
fmt.Println("test2 run... logId:", ctx.Value(LogIDKey))
ctx = context.WithValue(ctx, CallerKey, "test2")
test222(ctx)
}
func test222(ctx context.Context) {
fmt.Printf("test222 run... caller: %s, logId:%s\n", ctx.Value(CallerKey), ctx.Value(LogIDKey))
}
运行结果:
test1 run... logId: 123456
test111 run... caller: test1, logId:123456
test2 run... logId: 123456
test222 run... caller: test2, logId:123456
WithCancel
使用context包的
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)方法,会返回cancelCtx实例和CancelFunc函数。作用:通过调用返回的
CancelFunc,可以取消所有子goroutine执行。取消子goroutine执行的原理:实际就是利用channel发送取消信号,子goroutine必须监听channel。
const callerKey = "Caller"
var logger = log.Default()
func main() {
ctx := context.Background()
ctx, cancelFunc := context.WithCancel(ctx)
go method1(ctx)
go method2(ctx)
<-time.After(3 * time.Second)
cancelFunc()
for {
}
}
func method1(ctx context.Context) {
ctx = context.WithValue(ctx, callerKey, "method1")
go method3(ctx)
for {
logger.Println("method1 run...")
select {
case <-ctx.Done():
logger.Println("method1 end...")
return
default:
<-time.After(time.Second)
}
}
}
func method2(ctx context.Context) {
for {
logger.Println("method2 run...")
select {
case <-ctx.Done():
logger.Println("method2 end...")
return
default:
<-time.After(time.Second)
}
}
}
func method3(ctx context.Context) {
for {
logger.Println("method3 run...caller:", ctx.Value(callerKey))
select {
case <-ctx.Done():
logger.Println("method3 end...")
return
default:
<-time.After(time.Second)
}
}
}
运行结果:
3秒后,method1、method2、method3的goroutine都退出了。
WithTimeout
使用context的
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)方法,返回timerCtx实例和CancelFunc函数。作用:
通过调用返回的
CancelFunc函数主动取消或者超时自动取消所有子goroutine执行,实际也是利用channel发送取消信号,子goroutine必须监听channel。
const callerKey = "Caller"
var logger = log.Default()
func main() {
ctx := context.Background()
ctx, _ = context.WithTimeout(ctx, 3*time.Second)
go method1(ctx)
go method2(ctx)
for {
}
}
func method1(ctx context.Context) {
ctx = context.WithValue(ctx, callerKey, "method1")
go method3(ctx)
for {
logger.Println("method1 run...")
select {
case <-ctx.Done():
logger.Println("method1 end...")
return
default:
<-time.After(time.Second)
}
}
}
func method2(ctx context.Context) {
for {
logger.Println("method2 run...")
select {
case <-ctx.Done():
logger.Println("method2 end...")
return
default:
<-time.After(time.Second)
}
}
}
func method3(ctx context.Context) {
for {
logger.Println("method3 run...caller:", ctx.Value(callerKey))
select {
case <-ctx.Done():
logger.Println("method3 end...")
return
default:
<-time.After(time.Second)
}
}
}
运行结果:
3秒后,method1、method2、method3的goroutine都退出了。
Context接口及实现
Context接口:
建议看看源码,有注释和最佳实践,比如:
Done()和Value(key any) any的最佳实践
type Context interface {
// 返回一个Deadline,如果没设置Deadline,则ok返回false
Deadline() (deadline time.Time, ok bool)
// 返回一个channel。
// 若当前上下文「不能取消」,会返回nil
// 若当前上下文「已取消或超时」,则返回的是【已关闭】的channel
// 最佳实践:通常在select语句中使用Done
Done() <-chan struct{}
// 若Done未关闭,则返回nil。若Done已关闭(取消或超时导致关闭),则返回non-nil
Err() error
// 返回上下文中key对应的value。最佳实践:key通常是全局变量,且是unexported,且是可比较类型
Value(key any) any
}
go标准库自带的实现有:
- emptyCtx
// 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
- valueCtx
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val any
}
- cancelCtx
注意:当前ctx被取消,会取消所有实现了
canceler的子ctx
// 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 atomic.Value // of 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
}
- timerCtx
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
Context的特性
- 树形结构
-
并发安全
树形结构
Context整体是一个树形结构(多叉树),树的不同节点可能是不同Context的实现,每次调用WithCancel,WithValue,WithTimeout, WithDeadline实际就是创建一个子节点。
从源码中可以看到valueCtx、cancelCtx、timeCtx都需要一个父ctx,而emptyCtx是作为根节点使用的。
重点:
emptyCtx的作用就是作为根节点的
- 一个go程序只有两个context根节点,即只有两个
emptyCtx实例:background和todo
源码如下:
整个程序中只有
background和todo两个emptyCtx实例,不能再创建emptyCtx了
package context
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// 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.
func Background() Context {
return background
}
// 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).
func TODO() Context {
return todo
}
- 兄弟节点之间是独立的,要想一个ctx节点不影响另一个ctx节点,则这两个ctx节点必须在不同子树上
- 如果我们想要新起的goroutine不受当前ctx控制,可以基于
emptyCtx派生出新的ctx,然后传给新起的goroutine
示例:
func main() {
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
go method1(ctx)
for {
}
}
func method1(ctx context.Context) {
log.Default().Println("method1 start")
go method2(ctx)
// 基于emptyCtx派生出新ctx,而不是基于当前ctx派生出新ctx
ctx2, _ := context.WithTimeout(context.Background(), 5*time.Second)
go method3(ctx2)
for {
select {
case <-ctx.Done():
log.Default().Println("method1 exit")
return
case <-time.After(time.Second):
log.Default().Println("method1 run")
}
}
}
func method2(ctx context.Context) {
log.Default().Println("method2 start")
for {
select {
case <-ctx.Done():
log.Default().Println("method2 exit")
return
case <-time.After(time.Second):
log.Default().Println("method2 run")
}
}
}
func method3(ctx context.Context) {
log.Default().Println("method3 start")
for {
select {
case <-ctx.Done():
log.Default().Println("method3 exit")
return
case <-time.After(time.Second):
log.Default().Println("method3 run")
}
}
}
运行结果:可以看到method1和method2 goroutine同时退出了,而method3 goroutine还能继续运行。
源码
类图
emptyCtx
// 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 any) any {
return nil
}
emptyCtx实现了Context接口的所有方法,都是返回空。
valueCtx
valueCtx自己只实现了Context接口的Value方法,其他方法都继承自父ctx。
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val any
}
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case *emptyCtx:
return nil
default:
// 自定义实现的ctx
return c.Value(key)
}
}
}
// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int
// cancelCtx对Value方法的实现,timerCtx直接继承了cancelCtx对Value方法的实现
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
// emptyCtx对Value方法的实现
func (*emptyCtx) Value(key any) any {
return nil
}
总结:
Value方法获取key对应的值,若当前context节点没有该key,会递归往父节点找,直至根节点。
cancelCtx
cancelCtx自己实现了Context接口的Value、Done、Err方法,其他方法都继承自父ctx。
func (c *cancelCtx) Value(key any) any {
// 通过cancelCtxKey,可获取cancelCtx本身。
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// A CancelFunc may be called by multiple goroutines simultaneously.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func()
// 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 atomic.Value // of 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
}
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
// 这个接口相当于定义了一个「可取消」的context类型
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
// cancelCtx对Done方法的实现,懒初始化Done channel
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
CancelFunc实际是调用ctx的cancel方法
详解propagateCancel方法:
核心:
- 若父ctx已取消,则取消child
- 绑定父子关系。取消父ctx时,就能找到所有子ctx并取消
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
// 表示parent ctx是不可取消的,且父ctx继承的所有祖先节点也都不能取消
return
}
select {
case <-done:
// 若parent ctx已经取消了,则立马取消child
child.cancel(false, parent.Err())
return
default:
}
// 从parent ctx开始往根节点找cancelCtx
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// err非nil,表示parent ctx已经取消了,则立马取消child
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
// 绑定父子关系
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 处理自定义的ctx
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
// 取消child
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
看看cancelCtx实现的cancel方法:
核心:
- 关闭Done channel,此后
Done方法返回的是已关闭的channel。即Done方法返回的是已关闭channel,则表示当前ctx已取消
- 赋值err,此后
Err方法返回非nil。即Err方法返回的是非nil,则表示当前ctx已取消
- 递归关闭取消child
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 取消后,err不能是nil
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
// err非nil,表示当前ctx已取消,直接返回
c.mu.Unlock()
return
}
// 赋值非nil的err
c.err = err
d, _ := c.done.Load().(chan struct{})
if d == nil {
// 表示Done channel还未初始化过,直接存储closedchan
c.done.Store(closedchan)
} else {
// 关闭Done channel
close(d)
}
// 取消当前ctx的所有child,每个child又会往下递归取消自己的child
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
// 删除所有child
c.children = nil
c.mu.Unlock()
// 把当前ctx从父ctx那删除
if removeFromParent {
removeChild(c.Context, c)
}
}
// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
总结:
-
核心几个方法:
- propagateCancel:绑定父子关系,子是
canceler类型 - parentCancelCtx:获取最近的父cancelCtx
- cancel:取消当前ctx,并取消所有子ctx
- propagateCancel:绑定父子关系,子是
Done方法返回nil,表示「当前ctx + 所在的继承链上的所有祖先ctx」都不可取消
-
不会向Done channel发送数据,而是仅关闭Done channel,关闭后然后消费方消费就不再阻塞了
timerCtx
timerCtx自己只实现了Context接口的Deadline方法,其他方法都继承自父ctx。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
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)
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 {
// 等于nil表示当前ctx未取消,则创建一个计时器:等待一定时间后再取消
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// timerCtx对cancel方法的实现
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 委托cancelCtx,所以底层是cancelCtx
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 {
// 停止timer
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
总结:
- timerCtx的底层原理是cancelCtx,比cancelCtx多了
deadline和timer,重新实现了cancel方法:1. 通过cancelCtx实现取消 2.停止timer计时器
谈谈用ctx取消goroutine执行的原理
关键:
一个协程无法被外部协程取消,只能自己取消自己。
所以,利用ctx取消goroutine执行其实就是利用ctx发送一个取消信号,其他goroutine会监听到这个信号,至于要不要取消执行则由自己决定。
最佳实践
- 不建议使用
context传递关键参数,关键参数应该显式声明,不应该隐式处理,context中最好是携带token、traceId这类值。
自定义不可取消的ctx
ctx的Done()方法返回nil表示该ctx是不能cancel的,可以使用这种ctx摆脱父ctx的取消控制。
如下,自定义不可取消的ctx ValueOnlyCtx
// ValueOnlyCtx 只能获取值,不能被取消
type ValueOnlyCtx struct {
// 父ctx
context.Context
}
func (*ValueOnlyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*ValueOnlyCtx) Done() <-chan struct{} {
return nil
}
func (*ValueOnlyCtx) Err() error {
return nil
}
测试:
const LogID = "LogID"
var globalCtx = context.Background()
func init() {
globalCtx = context.WithValue(globalCtx, LogID, "test666")
globalCtx, _ = context.WithTimeout(globalCtx, 2*time.Second)
}
func TestT1(t *testing.T) {
// 此时异步任务【会】受ctx控制被取消
go asyncTask(globalCtx)
<-time.After(5 * time.Second)
}
func TestT2(t *testing.T) {
// 使用不可取消的ctx,使异步任务摆脱父ctx的取消控制
ctx := &ValueOnlyCtx{Context: globalCtx}
// 此时异步任务【不会】受ctx控制被取消
go asyncTask(ctx)
<-time.After(5 * time.Second)
}
func asyncTask(ctx context.Context) {
lodID := ctx.Value(LogID)
count := 0
// 任务耗时,正常情况下异步任务要执行这么长时间才完成
taskCostTime := time.After(3 * time.Second)
for {
<-time.After(500 * time.Millisecond)
count++
logger.Printf("LogID:%s asyncTask run %d", lodID, count)
select {
case <-taskCostTime:
logger.Println("asyncTask completed")
return
case <-ctx.Done():
logger.Println("asyncTask canceled")
return
default:
}
}
}
测试结果:
TestT1测试用例运行结果如下:
=== RUN TestT1
2023/03/22 00:23:59 LogID:test666 asyncTask run 1
2023/03/22 00:24:00 LogID:test666 asyncTask run 2
2023/03/22 00:24:00 LogID:test666 asyncTask run 3
2023/03/22 00:24:01 LogID:test666 asyncTask run 4
2023/03/22 00:24:01 asyncTask canceled
--- PASS: TestT1 (5.00s)
PASS
可以看到异步任务被取消
TestT2测试用例运行结果:
=== RUN TestT2
2023/03/22 00:25:36 LogID:test666 asyncTask run 1
2023/03/22 00:25:37 LogID:test666 asyncTask run 2
2023/03/22 00:25:37 LogID:test666 asyncTask run 3
2023/03/22 00:25:38 LogID:test666 asyncTask run 4
2023/03/22 00:25:38 LogID:test666 asyncTask run 5
2023/03/22 00:25:39 LogID:test666 asyncTask run 6
2023/03/22 00:25:39 asyncTask completed
--- PASS: TestT2 (5.00s)
PASS
可以看到异步任务正常完成,所以可以使用「自定义的不可取消的ctx」使异步任务摆脱父ctx的取消控制。
并发安全
多个goroutine能并发安全的读写ctx中的值,如何实现并发安全的?一个ctx能被多个goroutine并发安全的使用,如何实现并发安全的?
答案:Context是一个不可变对象,每次修改Context都是创建一个新结构体。
总结
- 了解了context的作用、应用、特性
- 看源码,学习思想、学习go语言编程
-
goroutine并发编程三大神器:channel、context以及sync包