本文共 6898 字,大约阅读时间需要 22 分钟。
在golang中goroutine是没有主从关系的,是平等的,也就是说goroutineA启动了goroutineB,即使goroutineA结束了,对goroutineB不会有影响,当然如果主程序结束了,所有的goroutine都会结束;在goalng作为服务运行时,各个goroutine的生命周期则需要人为来控制,否则容易出现goroutine泄露的情况。
Golang 的 context Package 提供了一种简洁又强大方式来管理 goroutine 的生命周期,同时提供了一种 Requst-Scope K-V Store。但是对于新手来说,Context 的概念不算非常的直观,这篇文章来带领大家了解一下 Context 包的基本作用和使用方法。
应用场景:在 Go http 包的 Server 中,每一个请求在都有一个对应的goroutine去处理。请求处理函数通常会启动额外的goroutine用来访问后端服务,比如数据库和 RPC 服务。用来处理一个请求的goroutine通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的 token、请求的截止时间。当一个请求被取消或超时时,所有用来处理该请求的goroutine都应该迅速退出,然后系统才能释放这些goroutine占用的资源。
在 go1.7 及以上版本 context 包被正式列入官方库中,所以我们只需要import "context"就可以了,而在 go1.6 及以下版本,我们要 import “golang.org/x/net/context”
通道ch被close(ch)之后,ch的读端会收到消息
p, ok := <-ch p的值是ch类型的零值 ok为false它是最基本的接口
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{ } Err() error Value(key interface{ }) interface{ }}
Deadline():返回一个time.Time,是当前 Context 的应该结束的时间,ok 表示是否有 deadlineDone():返回一个struct{ }类型的只读 channelErr():返回 Context 被取消时的错误Value(key interface{ }):是 Context 自带的 K-V 存储功能
为什么 Done() 方法要返回只读的通道呢?
这是出于安全考虑,防止此通道被写入数据从而导致协程退出,设置成只读的话,那就只有 close 这一个操作可以触发通道行为了。定义了提供 cancel 函数的 context,当然要求数据结构要同时实现 Context interface
type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{ }}
emptyCtx是空的Context,只实现了Context interface,只能作为 root context 使用。
type emptyCtx int
cancelCtx 继承了 Context 并实现了canceler interface,从WithCancel()函数产生
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 // set to non-nil by the first cancel call}
cancelCtx 中的 Context 元素为 Context 类型,也就是实现了 Context 接口的变量。
因此cancelCtx也实现了Context。为什么 cancelctx 还要重新实现 Done 方法呢?
假如传入一个 emptyCtx 作为基础ctx 得到 cancelctx,此时 cancelctx 自然实现了 Context 和 canceler 的 Done 方法,但是这是继承自 emptyCtx 的实现,是这样的:func (*emptyCtx) Done() <-chan struct{ } { return nil}
要知道 nil chan 是无法被使用的,否则 panic ,因此需要重新实现
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}
这里有一个细节,虽然 Done() 方法只需要返回可读通道,但是必须要 make 一个双向的通道,因为最终 close 一个只读的通道是会报错的,也就是说 c.done 必须为双向通道。
这也是 Done() 方法存在的意义,别看它只是简单的返回一个对象。
timerCtx继承了cancelCtx,所以也自然实现了Context和canceler这两个interface,由WithDeadline()函数产生
type timerCtx struct { cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time}
valueCtx包含key、val field,可以储存一对键值对,由WithValue()函数产生
type valueCtx struct { Context key, val interface{ }}
Context 只定义了 interface,真正使用时需要实例化,官方首先定义了一个 emptyCtx struct 来实现 Context interface,然后提供了Backgroud() 和 TODO() 函数来便利的生成一个 emptyCtx 实例。
type emptyCtx intvar ( background = new(emptyCtx) todo = new(emptyCtx))func Background() Context { return background}func TODO() Context { return todo}
Backgroud() 生成的 emptyCtx 实例是不能取消的,因为emptyCtx没有实现canceler interface,要正常取消功能的话,还需要对 emptyCtx 实例进行派生。常见的两种派生用法是WithCancel(),WithTimeout,WithDeadline。
emptyCtx 没有提供 cancel的 能力。cancelCtx 包含了emptyCtx,并提供 cancel 的能力。timerCtx 包含了 cancelCtx,提供了超时后自动调用 cancel 的能力,当然也可以手动 cancel 。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) }}// newCancelCtx returns an initialized cancelCtx.func newCancelCtx(parent Context) cancelCtx { return cancelCtx{ Context: parent}}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { 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) }}
传递一个截止的时间。
timerCtx 有一个 timer 属性,该属性对应一个定时器,
c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded)})
初始化一个定时器,到时自动执行 cancel 操作。
即便如此,出于安全考虑,我们还是应该在主代码手动执行 defer cancel()
。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout))}
传递一个超时时间time.Duration,和 WithDeadline 用途一样。
func WithValue(parent Context, key, val interface{ }) Context { if key == nil { panic("nil key") } if !reflect.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{ parent, key, val}}
context 包可以用来控制协程的退出,协程间共享数据。
一般是这样使用的
go func() { for { select { case <-ctx.Done(): return case xxx: yyy } }}()
示例:
func main() { C6()}func C6() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("ctx done") return default: // do some thing } } }(ctx) rand.Seed(time.Now().UnixNano()) for { n := rand.Intn(6) fmt.Println(n) if n == 5 { return } time.Sleep(1*time.Second) }}
程序结束了也没打印 ctx done ,为什么呢?
那时因为主程序结束了,一切都结束了。修改 main 方法:func main() { go C6() for { time.Sleep(1*time.Second) }}
这样就有了。
或许,你就要问了,就这,非得用context吗,我使用通道也能解决啊,因为关闭一个通道,所有的读端也都会收到消息。
func C7() { clo := make(chan struct{ }) defer close(clo) for i := 0; i < 3; i++ { // readonly chan go func(clo <-chan struct{ }) { for { select { case <-clo: fmt.Println("ctx done") return default: // do some thing } } }(clo) } rand.Seed(time.Now().UnixNano()) for { n := rand.Intn(6) fmt.Println(n) if n == 5 { return } time.Sleep(1 * time.Second) }}
一样的效果,那么context包的必要性在哪里呢?
一个超时的例子
func C2() { d := time.Now().Add(50 * time.Millisecond) ctx, cancel := context.WithDeadline(context.Background(), d) defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) }}
一个WithValue的例子
func C3() { type favContextKey string f := func(ctx context.Context, k favContextKey) { if v := ctx.Value(k); v != nil { fmt.Println("found value:", v) return } fmt.Println("key not found:", k) } k := favContextKey("language") ctx := context.WithValue(context.Background(), k, "Go") f(ctx, k) f(ctx, favContextKey("color"))}
context 就是一个工具,有助于我们更方便的实现以上这些功能,使程序显得优雅简洁,但并不是必须的。
转载地址:http://snaui.baihongyu.com/