golang上下文context

context上下文

文章目录

  • context上下文
    • 1. context简介
    • 2. context接口
    • 3. emptyCtx、cancelCtx、timerCtx和valueCtx
    • 4. Background() 和 TODO()
    • 5. WithCancel()、WithDeadline()、WithTimeout()和WithValue()
    • 6. 使用context小结

1. context简介

Go1.7加入了一个新的标准库context,它定义了Context类型,专门用来简化对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。

简而言之:context的作用就是一种多Goroutine管理机制。

context包不仅实现了在程序单元之间共享状态变量的方法,同时能通过简单的方法,使我们在被调用程序单元的外部,通过设置ctx变量值,将过期或撤销这些信号传递给被调用的程序单元。在网络编程中,若存在A调用B的API, B再调用C的API,若A调用B取消,那也要取消B调用C,通过在A,B,C的API调用之间传递context,以及判断其状态,就能解决此问题。

context包的主要内容可以被概括为1个接口4个具体实现6个函数

golang上下文context_第1张图片

2. context接口

context接口中提供了4个方法:

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}
  • Deadline会返回一个超时时间,Goroutine获得了超时时间后,例如可以对某些io操作设定超时时间。
  • Done方法返回一个通道(channel),当Context被撤销或过期时,该通道是关闭的,即它是一个表示Context是否已关闭的信号。
  • 当Done信道关闭后,Err方法表明Context被撤的原因。
  • Value可以让Goroutine共享一些数据,当然获得数据是协程安全的。但使用这些数据的时候要注意同步,比如返回了一个map,而这个map的读写则要加锁。

3. emptyCtx、cancelCtx、timerCtx和valueCtx

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

emptyCtx只是简单的返回nil和false等等,实际上什么也没做。一般用来当作context树的根节点。

cancelCtx

// 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
}

cancelCtx中有一个context变量作为父节点,变量done表示一个channel,用来表示传递关闭信号;children表示一个map,存储了当前context节点下的子节点;err用于存储错误信息表示任务结束的原因。

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
}

timerCtx是一种基于cancelCtx的context类型,从字面上就能看出,这是一种可以定时取消的context。

timerCtx内部使用cancelCtx实现取消,另外使用定时器timer和过期时间deadline实现定时取消的功能。timerCtx调用cancel方法,会先将内部的cancelCtx取消,如果需要则将自己从cancelCtx祖先节点上移除,最后取消计时器。

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 interface{}
}

valueCtx利用一个Context类型的变量来表示父节点context,所以当前context继承了父context的所有信息;valueCtx类型还携带一组键值对,也就是说这种context可以携带额外的信息。valueCtx实现了Value方法,用以在context链路上获取key对应的值,如果当前context上不存在需要的key,会沿着context链向上寻找key对应的值,直到根节点。

4. Background() 和 TODO()

对于goroutine,他们的创建和调用关系总是像层层调用进行的,就像一个树状结构,而更靠顶部的context应该有办法主动关闭下属的goroutine的执行。为了实现这种关系,context也是一个树状结构,叶子节点总是由根节点衍生出来的。

要创建context树,第一步应该得到根节点,context.Background()函数的返回值就是根节点。

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
}

这两个函数函数内部都会创建一个emptyCtx。

Background() 该函数返回空的Context,该Context一般由接收请求的第一个Goroutine创建,是与进入请求对应的Context根节点,它不能被取消、没有值、也没有过期时间。它常常作为处理Request的顶层context存在。

Background()函数返回的变量结构:

golang上下文context_第2张图片

TODO() 官方文档建议本来应该使用的外层传递的ctx,而外层却没有传递的地方使用,就像函数名称表达的一样,留下一个TODO。

5. WithCancel()、WithDeadline()、WithTimeout()和WithValue()

有了根节点,就可以创建子孙节点了,context包提供了一系列方法来创建他们:

// 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()

怎么通过context传递改变后的状态呢?
在父goroutine中可以通过Withxx方法获取一个cancel方法,从而获得了操作子context的权力。

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) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

WithCancel返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。
取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。

我们来了解一个利用context结束goroutine的demo。

package main

import (
	"context"
	"fmt"
	"time"
)

/*
	创建一个通道chan,启动goroutine
	for循环储存数据
 */
func gen(ctx context.Context) <-chan int {
	dst := make(chan int)
	n := 1
	go func() {
		for  {
			select {
				case <- ctx.Done() :
					return  //return结束该goroutine,防止泄露
				case dst <- n:
					n++
			}
		}
	}()
	return dst
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//当取数据n == 5的时候,执行defer cancel操作
	defer cancel()

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
 	}
	 time.Sleep(time.Second * 5)
}

运行结果:

1
2
3
4
5

上面的示例代码中,gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。

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 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)
	}
	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 {
		c.timer = time.AfterFunc(dur, func() {
			c.cancel(true, DeadlineExceeded)
		})
	}
	return c, func() { c.cancel(true, Canceled) }
}

WithDeadline函数的作用也差不多,它返回的Context类型值同样是parent的副本,但其过期时间由deadline和parent的过期时间共同决定。当parent的过期时间早于传入的deadline时间时,返回的过期时间应与parent相同。父节点过期时,其所有的子孙节点必须同时关闭;反之,返回的父节点的过期时间则为deadline。

package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	d := time.Now().Add(50 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
	// 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
	defer cancel()

	select {
		case <- time.After(1 * time.Second):
			fmt.Println("overslept")
		case <- ctx.Done():
		fmt.Println(ctx.Err())
	}
}

运行结果:

context deadline exceeded

上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。

在上面的示例代码中,因为ctx 50毫秒后就会过期,所以ctx.Done()会先接收到context到期通知,并且会打印ctx.Err()的内容。

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) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

WithTimeout函数与WithDeadline类似,只不过它传入的是从现在开始Context剩余的生命时长。他们都同样也都返回了所创建的子Context的控制权,一个CancelFunc类型的函数变量。

当顶层的Request请求函数结束后,我们就可以cancel掉某个context,从而层层Goroutine根据判断cxt.Done()来结束。

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
	for {
		fmt.Println("db connecting ...")
		time.Sleep(time.Millisecond * 10) //假设正常连接数据库耗时10毫秒
		select {
			case <- ctx.Done():  //50毫秒后自动调用
				break LOOP
			default:
		}
	}
	fmt.Println("worker done!")
	wg.Done()
}

func main() {
	//设置一个50毫秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond * 50)
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel()  //通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}

运行结果:

db connecting ...
db connecting ...
db connecting ...
db connecting ...
db connecting ...
worker done!
over

WithValue()

// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}

WithValue函数,它返回parent的一个副本,调用该副本的Value(key)方法将得到val。这样我们不光将根节点原有的值保留了,还在子孙节点中加入了新的值,注意若存在Key相同,则会被覆盖。

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
	key := TraceCode("TRACE_CODE")
	traceCode, ok := ctx.Value(key).(string)  //在子goroutine中获取trace code
	if !ok {
		fmt.Println("invalid trace code")
	}
LOOP:
	for {
		fmt.Printf("worker, trace code:%s\n", traceCode)
		time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
		select {
		case <-ctx.Done(): // 50毫秒后自动调用
			break LOOP
		default:
		}
	}
	fmt.Println("worker done!")
	wg.Done()
}

func main() {
	// 设置一个50毫秒的超时
	ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
	// 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
	ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancel() // 通知子goroutine结束
	wg.Wait()
	fmt.Println("over")
}

运行结果:

worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker, trace code:12512312234
worker done!
over

6. 使用context小结

小结:

  • context包通过构建树形关系的context,来达到上一层goroutine对下一层goroutine的控制。对于处理一个request请求操作,需要通过goroutine来层层控制goroutine,以及传递一些变量来共享。
  • context变量的请求周期一般为一个请求的处理周期。即针对一个请求创建context对象;在请求处理结束后,撤销此ctx变量,释放资源。
  • 每创建一个goroutine,要不将原有context传递给子goroutine,要么创建一个子context传递给goroutine.
  • Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。
  • 当通过父
    Context对象创建子Context时,可以同时获得子Context的撤销函数,这样父goroutine就获得了子goroutine的撤销权。

注意事项:

  • 推荐以参数的方式显示传递Context。
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()。
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数。
  • Context是线程安全的,可以放心的在多个goroutine中传递。

参考博客:
https://www.liwenzhou.com/posts/Go/go_context/#autoid-0-4-0

https://www.cnblogs.com/zhangboyu/p/7456606.html

https://www.cnblogs.com/juanmaofeifei/p/14439957.html

你可能感兴趣的:(go,golang)