1 简介
golang 中的创建一个新的 goroutine , 并不会返回像c语言类似的pid,我们不能从外部杀死某个goroutine,只能让它自己结束,之前我们用channel+select 的方式来解决这个问题,但是有些场景实现起来比较麻烦,例如由一个请求衍生出的各个 goroutine 之间需要满足一定的约束关系,以实现一些诸如有效期,中止routine树,传递请求全局变量之类的功能。于是google 就为我们提供一个解决方案,开源了context包。
context包不仅实现了在程序单元之间共享状态变量的方法,同时能通过简单的方法,使我们在被调用程序单元的外部,通过设置ctx变量值,将过期或撤销这些信号传递给被调用的程序单元。在网络编程中,若存在A调用B的API, B再调用C的API,若A调用B取消,那也要取消B调用C,通过在A,B,C的API调用之间传递Context ,以及判断其状态,就能解决此问题,这是为什么gRPC的接口中带上ctx context.Context参数的原因之一。
2 源码剖析
2.1 核心接口context.Context
// context 包里的方法是线程安全的,可以被多个 goroutine 使用
type Context interface {
// 当Context 被 canceled 或是 times out 的时候,Done 返回一个被 closed 的channel
Done() <-chan struct{}
// 在 Done 的 channel被closed 后, Err 代表被关闭的原因
Err() error
// 如果存在,Deadline 返回Context将要关闭的时间
Deadline() (deadline time.Time, ok bool)
// 如果存在,Value 返回与 key 相关了的值,不存在返回 nil
Value(key interface{}) interface{}
}
我们不需要手动实现这个接口,context 包已经给我们提供了两个,一个是 Background(),一个是 TODO(),这两个函数都会返回一个 Context 的实例。只是返回的这两个实例都是空 Context。
2.2 创建context后代的方法
// WithCancel对应的是cancelCtx,返回一个 cancelCtx和一个CancelFunc,CancelFunc是 context包中定义的一个函数类型:type CancelFunc func()。调用这个CancelFunc 时,关闭对应的c.done,也就是让他的后代goroutine退出。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// WithDeadline和WithTimeout对应的是timerCtx ,WithDeadline和WithTimeout是相似的,WithDeadline 是设置具体的deadline时间,到达deadline的时候,后代 goroutine退出,而WithTimeout简单粗暴,直接return WithDeadline(parent, time.Now().Add(timeout))。
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// WithValue对应valueCtx,WithValue是在Context 中设置一个map,拿到这个Context以及它的后代的goroutine 都可以拿到map里的值。
func WithValue(parent Context, key interface{}, val interface{}) Context
2.3 context方法实现
cancelCtx结构体继承了Context,实现了canceler方法。valueCtx和timerCtx结构体继承了cancelCtx, 都实现了canceler接口。
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
type cancelCtx struct {
Context
done chan struct{} // closed by the first cancel call.
mu sync.Mutex
children map[canceler]bool // set to nil by the first cancel call
err error // 当其被cancel时将会把err设置为非nil
}
func (c *cancelCtx) Done() <-chan struct{} {
return c.done
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
defer c.mu.Unlock()
return c.err
}
func (c *cancelCtx) String() string {
return fmt.Sprintf("%v.WithCancel", c.Context)
}
//核心是关闭c.done
//同时会设置c.err = err, c.children = nil
//依次遍历c.children,每个child分别cancel
//如果设置了removeFromParent,则将c从其parent的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
close(c.done)
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) // 从此处可以看到 cancelCtx的Context项是一个类似于parent的概念
}
}
// timerCtx 结构继承 cancelCtx
type timerCtx struct {
cancelCtx //此处的封装为了继承来自于cancelCtx的方法,cancelCtx.Context才是父亲节点的指针
timer *time.Timer // Under cancelCtx.mu. 是一个计时器
deadline time.Time
}
// valueCtx 结构继承 cancelCtx
type valueCtx struct {
Context
key, val interface{}
}
3 context使用
3.1 使用原则
context的创建和调用关系总是像层层调用进行的,就像人的辈分一样,为了实现这种关系,Context结构也应该像一棵树,叶子节点须总是由根节点衍生出来的。
一般创建context根节点使用context.Background(),函数返回空的Context,有了根节点,又该怎么创建其它的子节点、孙节点呢?使用WithCancel、WithDeadline、WithTimeout、WithValue这四个函数可以在根节点基础上创建子节点、孙节点……
一般使用原则:
- 不要把Context 存在一个结构体当中,显式地传入函数。Context 变量需要作为第一个参数使用,一般命名为ctx(列如:func Add(ctx context.Context, a, b int) int)
- 即使方法允许,也不要传入一个nil的Context ,如果你不确定你要用什么 Context的时候传一个context.TODO。
- 使用context的Value 相关方法只应该用于传递和请求相关的元数据,不要用它来传递一些可选的参数. -同样的Context可以用来传递到不同的goroutine 中,Context 在多个goroutine中是安全的.
3.2 使用示例
package main
import (
"fmt"
"golang.org/x/net/context"
"math/rand"
"time"
)
// 模拟一个随机延时的阻塞函数
func randSleep(ctx context.Context) error {
rand.Seed(time.Now().UnixNano())
n := time.Duration(rand.Intn(5000))
fmt.Printf("time sleep %d millisecond\n", n)
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(n * time.Millisecond):
}
return nil
}
// 求两个数的平方和,如果计算被中断,则返回 -1
func quadraticSum(ctx context.Context, a, b int) (int, error) {
select {
case <-ctx.Done():
return -1, ctx.Err()
default: // 处理
err := randSleep(ctx)
if err != nil {
return -1, err
}
}
return a*a + b*b, nil
}
// 定时取消执行
func autoCancel() {
ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) // 设置deadline时间为2秒
a, b := 1, 2
result, err := quadraticSum(ctx, a, b) // 当执行时间超过2秒,自动取消
if err != nil {
fmt.Printf("autoCancel error, %v\n", err)
} else {
fmt.Printf("autoCancel: %d*%d + %d*%d = %d\n", a, a, b, b, result)
}
}
// 手动取消
func manualCancel() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(3 * time.Second)
cancel() // 3秒后主动取消
}()
a, b := 3, 4
result, err := quadraticSum(ctx, a, b)
if err != nil {
fmt.Printf("manualCancel error, %v\n", err)
} else {
fmt.Printf("manualCancel: %d*%d + %d*%d = %d\n", a, a, b, b, result)
}
}
func main() {
go autoCancel()
time.Sleep(10 * time.Millisecond)
go manualCancel()
time.Sleep(10 * time.Second)
}
参考:
https://studygolang.com/articles/9517
https://www.cnblogs.com/DaBing0806/p/6878810.html
专题「golang相关」的其它文章 »
- 使用开发框架sponge快速把单体web服务拆分为微服务 (Sep 18, 2023)
- 使用开发框架sponge一天多开发完成一个简单版社区后端服务 (Jul 30, 2023)
- 一个提高开发项目效率的开发框架sponge,支持mysql、mongodb、postgresql、tidb、sqlite (Jan 06, 2023)
- go test命令 (Apr 15, 2022)
- go应用程序性能分析 (Mar 29, 2022)
- channel原理和应用 (Mar 22, 2022)
- go runtime (Mar 14, 2022)
- go调试工具 (Mar 13, 2022)
- cobra (Mar 10, 2022)
- grpc使用实践 (Nov 27, 2020)
- 配置文件viper库 (Nov 22, 2020)
- 根据服务名称查看golang程序的profile信息 (Sep 03, 2019)
- go语言开发规范 (Aug 28, 2019)
- goroutine和channel应用——处理队列 (Sep 06, 2018)