Ezreal 书架 Ezreal 书架
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
Home
  • 《Go程序员面试笔试宝典》
  • 《RabbitMQ 实战指南》
  • 《深入理解kafka》
  • MySQL45讲
  • 透视HTTP协议
  • 结构化数据的分布式存储系统
  • Raft 共识算法
  • 逃逸分析

  • 延迟语句

  • 数据容器

  • 通道

  • 接口

  • unsafe

  • context

    • context 是什么
    • context 有什么作用
    • 如何使用 context
    • context 底层原理是什么
      • 接口
        • Context
        • Canceler
      • 结构体
  • Go程序员面试笔试宝典
  • context
ezreal_rao
2023-06-01
目录

context 底层原理是什么

context 包的代码并不长,context.go 文件总共不到 500 行,其中还有很多大段的注释,代码 可能也就 200 行左右的样子,是一个非常值得研究的代码库。

先看一张整体的图,如图 7-3 所示,

context 包代码结构

其具体功能见表 7-1。

context 包代码结构功能

上面这张表展示了 context 的 所有函数、接口、结构体。 整体类图如图 7-4 所示。

context 整体类图

# 接口

# Context

直接看源码:

// src/context/context.go
type Context interface {
    // 返回 context 是否会被取消以及自动取消时间(即 deadline)
    Deadline() (deadline time.Time, ok bool)
    // 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
    Done() <-chan struct{}
    // 在 channel Done 关闭后,返回 context 取消原因
    Err() error
    // 获取 key 对应的 value
    Value(key interface{}) interface{}
}
1
2
3
4
5
6
7
8
9
10
11

1)Context 是一个接口,定义了 4 个方法,它们都是幂等的,意味着连续多次调用同一个方法,得到的结果都是相同的。

2)Deadline () 返回 context 的截止时间,通过此时间,用户就可以决定是否进行接下来的操作,如果时间太短,就可以不往下做了,否则浪费系统资源。当然,也可以用这个 deadline 来设置 一个 I/O 操作的超时时间。

3)Done () 返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时, 说明 context 被取消了。注意,这是一个只读的 channel。并且,读一个关闭的 channel 会读出相应类型的零值,而源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 receive-only 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出。

4)Err () 返回一个错误,表示 channel 被关闭的原因。例如被取消,还是超时。

5)Value () 获取之前设置的 key 对应的 value。

# Canceler

再来看另外一个接口:

// src/context/context.go

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}
1
2
3
4
5
6

对于实现了上面定义的两个方法的 context,就表明是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意它们是加了 * 号的,即这两个结构体的指针类型实现了 canceler 接口。

canceler 接口设计成这样的原因:

1)“取消” 操作应该是建议性,而非强制性。

Caller 不应该去关心、干涉 callee 的情况, 决定如何以及何时 return 是 callee 的责任。 Caller 只需发送 “取消” 信号,callee 根据收到的信号来做进一步的决策,因此 Context 接口并没 有定义 cancel 方法。

2)“取消” 操作应该可传递

“取消” 某个函数时,和它相关联的其他函数也应该 “取消”。因此,Done () 方法返回一个只读的 channel,所有相关函数监听同一个 channel。一旦 channel 关闭,通过 channel 的 “广播机制”,所有监听者都能收到 “取消” 信号。

# 结构体

源码中定义了 Context 接口,给出了一个 emptyCtx 的实现:

// src/context/context.go

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 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"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

每个函数都实现的异常简单,要么是直接返回,要么是返回 nil。所以,这实际上是一个空的 context,永远不会被 cancel,没有存储值,也没有 deadline。

它被包装成:

var (
    background = new(emptyCtx)
    todo = new(emptyCtx)
)
1
2
3
4

通过下面两个导出的函数(首字母大写)对外公开:

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}
1
2
3
4
5
6
7

变量 background 通常用在顶层函数中,作为所有 context 的根节点。

变量 todo 通常用在不知道传递什么 context 的情形。例如,调用一个需要传递 context 参数的函数,但手头并没有其他 context 可以传递,这时就可以传递 todo。这常常发生在进行重构的过程中,给一些函数添加了一个 context 参数,但不知道要传什么,就用 todo “占个位子”,但最终要换成其他有意义的 context。

再来看一个重要的 context,即 cancelCtx:

// src/context/context.go

type cancelCtx struct {
    Context
    // 保护之后的字段
    mu sync.Mutex
    done chan struct{}
    children map[canceler]struct{}
    err error
}
1
2
3
4
5
6
7
8
9
10

这是一个可以取消的 context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。

先来看 Done () 方法的实现:

// src/context/context.go

func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}
1
2
3
4
5
6
7
8
9
10
11

函数中,c.done 是 “懒汉式” 创建,只有调用了 Done () 方法的时候才会被创建。再次强调, 函数返回的是一个只读的 channel,而且源码中没有地方向这个 channel 里面写数据。所以,直接读这个 channel 的协程会被 block。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。

Err () 和 String () 方法比较简单,不详细解释。接下来,重点关注 cancel () 方法的实现:

// src/context/context.go

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    // 必须要传 err
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // 已经被其他协程取消
    }
    // 给 err 字段赋值
    c.err = err
    // 关闭 channel,通知其他协程
    if c.done == nil {
        // 相当于没有调 Done() 方法,所以不需要真正关闭 c.done,而且 c.done 是 nil
        c.done = closedchan
    } else {
        close(c.done)
    }

    // 遍历它的所有子节点
    for child := range c.children {
        // 递归地取消所有子节点
        child.cancel(false, err)
    }
    // 将子节点置空
    c.children = nil
    c.mu.Unlock()
    if removeFromParent {
        // 从父节点中移除自己
        removeChild(c.Context, c)
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

总体来看,cancel () 方法的功能就是关闭 c.done;递归地取消它的所有子节点;从父节点中删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。一般地,goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 分支被选中。

再来看创建一个可取消的 context 的方法:

// src/context/context.go

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

func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

WithCancel 是一个暴露给用户的方法,传入一个父 context(这通常是一个 background,作为根节点),返回新创建的 context,新 context 的 done channel 是新建的。

当 WithCancel 函数返回的 CancelFunc 被调用或者是父节点的 done channel 被关闭(父节点 的 CancelFunc 被调用),此 context(子节点) 的 done channel 也会被关闭。

注意传给 WithCancel 方法的参数,前者是 true,也就是说取消的时候,需要将自己从父节点里删除。第二个参数则是一个固定的取消错误类型:

var Canceled = errors.New("context canceled")
1
#golang#context
上次更新: 7/12/2024, 2:37:05 PM
如何使用 context

← 如何使用 context

最近更新
01
为什么我的MySQL会抖一下
07-15
02
HTTP 性能优化面面观
07-12
03
WebSocket:沙盒里的 TCP
07-12
更多文章>
Theme by Vdoing | Copyright © 2022-2024 Ezreal Rao | CC BY-NC-SA 4.0
豫ICP备2023001810号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式