context.Context
- 前言
- 一、为什么要context
- 二、context有什么用
- 三、基本数据结构
- 3.1、context包的整体工作机制
- 3.2 基本接口和结构体
- 3.3 API函数
- 3.4 辅助函数
- 3.5 context用法
- 3.6 使用 context 传递数据的争议
- 总结
- 参考资料
前言
context是go语言的一个并发包,一个标准库,用于goroutine之间的退出通知。
一、为什么要context
Go中的goroutine之间没有父与子的关系,也就没有所谓子进程退出后的通知机制,多个goroutine被并行的调度,多个goroutine如何协作?
- 利用select收敛 + 在输入端在绑定一个非业务chan。
这样处理某个goroutine是一个简单的方案,但这不是一个通用的解决方案,当并发结构非常复杂时,该方案就显得力不从心了。
实际编程中 goroutine 会拉起新的 goroutine ,新的 goroutine 又会拉起另一个新的 goroutine ,最终形成一个树状的结构,由于 goroutine 里并没有父子的概念, 这个树状的结构只是在程序员头脑中抽象出来的 ,程序的执行模型并没有维护这么一个树状结构 。怎么通知这个树状上的所有 goroutine 退出?仅依靠语法层面的支持显然比较难处理。
- 为此 Go 1.7 提供了一个标准库 context 来解决这个问题。它提供两种功能: 退出通知和元数据传递。 context库 的设计目的就是跟踪 goroutine 调用 , 在其内部维护一个调用树, 井在这些调用树中传递通知和元数据。
二、context有什么用
context 库的设计目的就是跟踪 goroutine 调用树,并在这些 gouroutine 调用树中传递通知和元数据。两个目的 :
- 退出通知机制一一通知可以传递给整个 goroutine 调用树上的每一个 goroutine 。
- 传递数据一一数据可 以传递给整个 goroutine 调用树上的每一个 goroutine。
三、基本数据结构
3.1、context包的整体工作机制
第一个创建 Context 的 goroutine被称为 root 节点。 root 节点负责创建一个实现 Context 接口的具体对象, 并将该对象作为参数传递到其新拉起的 goroutine , 下游的 goroutine 可 以继续封装该对象,再传递到更下游的 goroutine 。Context 对象在传递的过程中最终形成一个树状的数据结构,这样通过位于 root 节点(树的根节点) 的 Context 对象就能遍历整个 Context 对象树 , 通知和消息就可以通过 root 节点传递出去 ,实现了上游 goroutine 对下游 goroutine 的消息传递。
3.2 基本接口和结构体
- Context 接口,Context 是一个基本接 口 , 所有的 Context 对象都要实现该接 口, context 的使用者在调用接口中都使用 Context 作为参数类型 。
type Context interface {
//如果 Context 实现了起时控制,则该方法返回 ok true, deadline 为超时时间,否则 ok 为 false
Deadline() (deadline time.Time, ok bool)
// 后端被调的 goroutine 应该监听该方法返回的 chan ,以便及时释放资源
Done() <-chan struct{}
//Done 返回的 ch an 收到通知的时候,才可以访问 Err ()获知因为什么原因被取消
Err() error
// 可以 访问上游 goroutine 传递给下游 goroutine 的位
Value(key interface{}) interface{}
}
- canceler 接口,canceler 接口是一个扩展接口,规定了取消通知的 Context 具体类型需要实现的接口 。
context 包中的具体类型*cancelCtx 和*timerCtx 都实现了该接口。
// 一个 context 对象如采实现了 canceler 接口,则可以被取消
type canceler interface {
// 创建 cancel 接 口实例的 g o routine 调 用 cancel 方法通知后续创建的 gorou tine 退出
cancel(removeFromParent bool, err error)
// Done 方法返回的 chan 需妥后端 goroutine 来监听 , 并及时退出
Done() <-chan struct{}
}
- empty Context结构体,emptyCtx 实现了 Context 接口,但不具备任何功能,因为其所有的方法都是空实现。其存在的目的是作为 Context 对象树的根( root 节点)。因为 context 包的使用思路就是不停地调用context 包提供的包装函数来创建具有特殊功能的 Context 实例 ,每一个 Context 实例的创建都以上一个 Context 对象为参数, 最终形成一个树状的结构。
type emptyCtx int
// 实现Context所有方法,就实现了Context
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 interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
package 定义了两个全局变量和两个封装函数,返回两个 emptyCtx 实例对象,实际使用时通过调用这两个封装函数来构造 Context 的 root 节点。
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
}
- cancelCtx结构体,cancelCtx 是一个实现了 Context 接口的具体类型,同时实现了 conceler 接口。 conceler 具有退出通知方法。注意退出通知机制不但能通知自己,也能逐层通知其 children 节点。
// cancelCtx 可以被取消, cancelCtx 取消时会同时取消所有实现 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
}
func (c *cancelCtx) Value(key interface{}) interface{} {
if key == &cancelCtxKey {
return c
}
return c.Context.Value(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
}
type stringer interface {
String() string
}
func contextName(c Context) string {
if s, ok := c.(stringer); ok {
return s.String()
}
return reflectlite.TypeOf(c).String()
}
func (c *cancelCtx) String() string {
return contextName(c.Context) + ".WithCancel"
}
// 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) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d) // 显示通知自己
}
// 通知孩子
// 循环调用 children 的 cancel 函数,由于 parent 已经取消,所以此时 child 调用cancel 传入的是 false
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
- timeCtx,timerCtx 是一个实现了 Context 接口 的具体类型 ,内部封装了 cancelCtx 类型实例 ,同时有一个 deadline 变量,用来实现定时退出通知。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
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 {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
- valueCtx,valueCtx 是一个实现了 Context 接口的具体类型,内部封装了 Context 接口类型,同时封装了一个 k/v 的存储变量。 valueCtx 可用来传递通知信息。
type valueCtx struct {
Context
key, val interface{}
}
// stringify tries a bit to stringify v, without using fmt, since we don't
// want context depending on the unicode tables. This is only used by
// *valueCtx.String().
func stringify(v interface{}) string {
switch s := v.(type) {
case stringer:
return s.String()
case string:
return s
}
return "<not Stringer>"
}
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
3.3 API函数
- 构造root节点对象,用于后序With包装函数的实参。
func Background() Context
func TODO() Context
- With包装函数
// 带退出通知的Context
func WithCancel (parent Context) (ctx Context , cancel CancelFunc)
// 带超时通知的Context
func WithDeadline (parent Context , deadline time.Time ) (Context , CancelFunc)
func WithTimeout (parent Context , timeout time.Duration) (Context ,
CancelFunc )
// 带传递数据的Context
func WithValue(parent Context , key , val interface{}) Context
这些函数都有一个共同的特点-- parent 参数,其实这就是实现 Context 通知树的必备条件。在 goroutine 的调用链中, Context 的实例被逐层地包装并传递,每层又可以对传进来的 Context实例再封装自己所需的功能 ,整个调用树需要一个数据结构来维护,这个维护逻辑在这些包装函数内部实现。
3.4 辅助函数
前面描述的 With 开头的构造函数是给外部程序使用的 API 接口函数。 Context 具体对象的链条关系是在 With 函数的内 部维护的。现在分析一下 With 函数内部使用的通用函数。
func propagateCancel(parent Context , child canceler )有如下几个功能:
- 判断 parent 的方法 Done() 返回值是否是 nil ,如果是,则说明 parent 不是一个可取消的 Context 对象,也就无所谓取消构造树,说明 child 就是取消构造树的根。
- 如果 parent 的方法 Done() 返回值不是 nil ,则向上回溯自己的祖先是否是 cancerCtx 类型实例,如果是,则将 child 的子节点注册维护到那棵关系树里面。
- 如果向上回溯自己的祖先都不是 cancelCtx 类型实例,则说明整个链条的取消树是不连续的。此时只需监听 parent 和自己的取消信号即可。
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
func parentCancelCtx(parent Context )(*cancelCtx , bool ) : 判断 parent 中 是否封装*cancelCtx 的字段,或者接口里面存放的底层类型是否是*cancelCtx 类型。
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
func removeChild(parent Context , child canceler ) : 如果 parent 封装*cancelCtx类型字段,或者接口里面存放的底层类型是*cancelCtx 类型 ,则将其构造树上的 child 节点删除。
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()
}
3.5 context用法
package main
import (
"context"
"fmt"
"time"
)
// context 基本用法
// 自定义context类型
type otherContext struct {
context.Context
}
// 主函数
func main() {
// 利用withCancel函数构建可取消的context
ctxA, cancel := context.WithCancel(context.Background())
// work 模拟运行,并检测前端的推出通知
go work(ctxA, "work1")
// 利用withDeadline函数构建超时通知的context
ctxB, _ := context.WithDeadline(ctxA, time.Now().Add(3*time.Second))
// work 模拟运行
go work(ctxB, "work2")
// 利用withValue函数构建可传递数据的context
oc := otherContext{ctxB}
go work(oc, "work3")
ctxC := context.WithValue(oc, "key", "andes pass from main")
// workWithValue 模拟运行
go workWithValue(ctxC, "work3")
// 故意停顿10s,让work2 和 work3 超时退出
time.Sleep(time.Second * 10)
// 让work1 cancel
cancel()
// 停顿5秒,让work1打印信息
time.Sleep(time.Second * 5)
fmt.Println("main stop")
}
// 获取Context传递的数据,等待前端的退出通知。
func workWithValue(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s get msg to cancel\n", name)
return
default:
val := ctx.Value("key").(string)
fmt.Printf("%s is running and value = %s\n", name, val)
time.Sleep(time.Second * 100)
}
}
}
// 检测通知 & do something
func work(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s get msg to cancel\n", name)
return
default:
fmt.Printf("%s is running\n", name)
time.Sleep(4 * time.Second)
}
}
}
3.6 使用 context 传递数据的争议
- 该不该使用 context 传递数据
首先要清楚使用 context 包主要是解决 goroutine 的通知退出,传递数据是其一个额外功能。可以使用它传递一些元信息 ,总之使用 context 传递的信息不能影响正常的业务流程,程序不要期待在 context 中传递一些必需的参数等,没有这些参数,程序也应该能正常工作。 - 在 context 中传递数据的坏处
- 传递的都是 interface{} 类型的值,编译器不能进行严格的类型校验。
- 从 interface{} 到具体类型需要使用类型断言和接口查闹,有一定的运行期开销和性能损失。
- 值在传递过程中有可能被后续的服务覆盖,且不易被发现。
- 传递信息不简明,较晦涩;不能通过代码或文档一眼看到传递的是什么,不利于后续维护。
- context 应该传递什么数据
- 日志信息。
- 调试信息
- 不影响业务主逻辑的可选数据。
context 包提供的核心的功能是多个 goroutine 之间的退出通知机制,传递数据只是一个辅助功能,应谨慎使用 context 传递数据。
总结
1)context 库的设计目的就是跟踪 goroutine 调用树,并在这些 gouroutine 调用树中传递通知和元数据。
参考资料
[1] Go 语言核心编程