天天看点

074-Context

上一篇文章结束我留下了一下小小的悬念,说 Golang 标准库为我们提供了纯天然的取消并发请求的解决方案,在这里我就需要填坑了。

1. 取消并发请求

为了能让你快速上手 context,我们继续延续上一篇文章的取消并发请求的例子,使用 context 将其改写。如果你不熟悉上一篇的例子,请务必回去再看一遍。

package main

import (
    "context"
    "fmt"
    "math/rand"
    "os"
    "sync"
    "time"
)

// Get 的参数由 channel 换成了 context
func Get(ctx context.Context) string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    select {
    case <-tick:
        return fmt.Sprintf("get page %d", duration)
    // 监控 context
    case <-ctx.Done():
        return fmt.Sprintf("cancel %d, reason:%v", duration, ctx.Err())
    }
}

func main() {
    rand.Seed(time.Now().Unix())
    // 创建一个 context 对象
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup
    go func() {
        os.Stdin.Read(make([]byte, 1))
        // 取消 context
        cancel()
    }()
    wg.Add(3)
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    wg.Wait()
}      

这段程序同样可以达到效果。实际上,上面的 Context 和之前我们使用 close channel 的功能是一样的,甚至 Context 底层的实现,也使用 close channel,只不过,Context 将这些操作都进行了封装。

可能你会问,这不是将事情复杂化了吗?看起来它并没有使用 close channel 更加方便。看起来似乎是这样,不过,Context 如果只有这点功能,那的确不如直接使用 close channel。如果你仔细看 Context 的名字,它似乎和 cancel 请求并没多大联系,只不过它提供了这样的功能。

2. Context 介绍

在服务器开发中,通常每个请求都会开启一个 goroutine 去执行(多线程模型思想)。在每个请求里,通常还会再开启额外的 goroutine 用来访问下游服务(典型的比如请求数据库等)。这些额外的和请求相关的 goroutine 可能需要一些特定的参数或值,比如本次请求的标识(ID),用户的 session 信息。

在 C/C++ 服务器开发中,通常可以借助 TLS (线程局部存储) 来存储相关的信息。而在 golang 里,可以使用 Context 对象来完成类似的功能。

另一方面,当请求超时或者被取消时,所有处理该请求的 goroutine 都应迅速退出,以便系统能回收它们正在使用的任何资源。

谷歌发明了 context 包,它可以很容易的将 request-scoped 相关的值、取消信号、过期时间通过 api 接口传递给所有的处理本次请求相关的 goroutines。

正如第 1 节中的示例一样,我们使用了 context 来完成所有 goroutine 的取消。

Context 当然远不止有取消请求的功能。

2.1 Context 接口

在 context 包中,Context 是一个接口类型,定义如下:

type Context interface {
    // 返回 context 截止时间。如果没有设置截止时间,则 ok == false
    Deadline() (deadline time.Time, ok bool)
    // 如果 context 被取消,Done 返回的 channel 则被关闭。如果 context 无法被取消,则 Done 返回 nil.
    Done() <-chan struct{}
    // context 被取消的原因
    Err() error
    // 获取该 context 中保存的 key-value
    Value(key interface{}) interface{}
}      

首先需要明确一点的是:

  • Context 是接口
  • 实现了 Context 接口的类型,不一定非得要完成所有的功能,比如截止时间。

下面是一个最简单的实现了 Context 接口的类型 emptyCtx:

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"      

当然了,这个 Context 对象几乎什么也没做。不过,这也为我们自己编写属于自己的 Context 提供了一些便利。比如:

type CancelContext struct {
    Context // 内嵌 Context 接口
    //...
}

// 继承一个 emptyCtx,看起来有点像 Nodejs 的原型链继承      

当然了,实际编码中,我们基本上不必自己再去实现一个 Context 对象了。context 包里已经为我们提供了一些已经实现好了的 Context 类型。主要是以下几种:

  • CancelContext: 支持取消功能的 Context
  • DeadlineContext: 支持到期自动取消的 Context
  • TimeoutContext: 支持超时自动取消的 Context
  • ValueContext: 能保存 key-value 的 Context

你可以通过下面 4 个函数来生成不同功能的 Context 对象:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context      

如果你自己看看上面这些函数的第一个参数,你会发现,它也是一个 Context 对象。这种利用父 context 来生成子 context 的思想,看起来简直和 Nodejs 中的原型继承一模一样(没了解过 Nodejs 的就忽略这句话啦)。

既然如此,总得有一个“上帝 Context”吧,这个上帝 Context 你完全可以自己折腾一个出来,当然你也可以使用上面我们写好的 emptyCtx 的对象。不过,Context 包已经为我们考虑好了,造轮的事情你也不用操心,你可以使用下面的函数来拿到一个 emptyCtx 对象:

func      

一切 Context 都是由这个 emptyCtx 繁衍而来。这看起来像“道生一,一生二,二生三,三生万物”的思想。“道”,指的就是那个 Background 函数。

2.2 Context 树

基于上面的思想,通过不断的基于 parent context 来繁衍新的 context 对象,最后形成的就是一棵 context 树。

074-Context

图1 河流分支

这意味着如果顶层的 Context 被 cancel,那么所有的子 Context 也会被 cancel. 有人会问,为什么不使用同一个 Context 对象呢,不也是一样的功能吗?正如第 1 节中的那样,只用一个 CancelContext 不就好了。

有一种情况,比如我只想取消第 1 节中的某一个 Get 请求,你要怎么办呢?我只想取消子 Context,而其它不受影响。这时候就能体现 Context 树的必要性了。

3. 使用 Context 的示例

使用 CancelContext 的例子在第一节已经展示过了,这里就不再展示。下面再举一个使用 TimeoutContext 和 ValueContext 的例子。

3.1 TimeoutContext

package main

import (
    "context"
    "errors"
    "fmt"
    "math/rand"
    "os"
    "sync"
    "time"
)

func Get(ctx context.Context) string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    select {
    case <-tick:
        return fmt.Sprintf("get page %d", duration)
    case <-ctx.Done():
        return fmt.Sprintf("cancel %d, reason:%v", duration, ctx.Err())
    }
}

func main() {
    rand.Seed(time.Now().Unix())
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    var wg sync.WaitGroup
    go func() {
        os.Stdin.Read(make([]byte, 1))
        context.Canceled = errors.New("手动取消请求")
        cancel()
    }()
    wg.Add(3)
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    wg.Wait()
}      
074-Context

图2 TimeoutContext

3.2 ValueContext

package main

import (
    "context"
    "errors"
    "fmt"
    "math/rand"
    "os"
    "sync"
    "time"
)

func Get(ctx context.Context) string {
    duration := rand.Intn(5) + 2
    tick := time.After(time.Duration(duration) * time.Second)
    select {
    case <-tick:
        return fmt.Sprintf("get page %d, session_id:%s, name:%s",
            duration, ctx.Value("session_id").(string), ctx.Value("name").(string))
    case <-ctx.Done():
        return fmt.Sprintf("cancel %d, reason:%v", duration, ctx.Err())
    }
}

func main() {
    rand.Seed(time.Now().Unix())
    // 先生成 CancelContext
    ctx, cancel := context.WithCancel(context.Background())
    // 生成 ValueContext
    ctx = context.WithValue(ctx, "session_id", "12345678")
    var wg sync.WaitGroup
    go func() {
        os.Stdin.Read(make([]byte, 1))
        context.Canceled = errors.New("手动取消请求")
        cancel()
    }()
    wg.Add(3)
    go func() {
        // 再为本次请求生成一个 ValueContext
        ctx := context.WithValue(ctx, "name", "allen")
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        ctx := context.WithValue(ctx, "name", "luffy")
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    go func() {
        ctx := context.WithValue(ctx, "name", "zoro")
        fmt.Println(Get(ctx))
        wg.Done()
    }()
    wg.Wait()
}      
074-Context

图3 ValueContext

下面是 context 树:

074-Context

4. 总结

  • 掌握 Context 的作用
  • 理解 Context 树