博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入理解 golang 中的 context
阅读量:3986 次
发布时间:2019-05-24

本文共 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占用的资源。

1. 包的引入

在 go1.7 及以上版本 context 包被正式列入官方库中,所以我们只需要import "context"就可以了,而在 go1.6 及以下版本,我们要 import “golang.org/x/net/context”

通道ch被close(ch)之后,ch的读端会收到消息

p, ok := <-ch
p的值是ch类型的零值
ok为false

2、基本数据结构

2-1 Context interface

它是最基本的接口

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 这一个操作可以触发通道行为了。

2-2 canceler interface

定义了提供 cancel 函数的 context,当然要求数据结构要同时实现 Context interface

type canceler interface {
cancel(removeFromParent bool, err error) Done() <-chan struct{
}}
emptyCtx

emptyCtx是空的Context,只实现了Context interface,只能作为 root context 使用。

type emptyCtx int
cancelCtx

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

timerCtx继承了cancelCtx,所以也自然实现了Context和canceler这两个interface,由WithDeadline()函数产生

type timerCtx struct {
cancelCtx timer *time.Timer // Under cancelCtx.mu. deadline time.Time}
valueCtx

valueCtx包含key、val field,可以储存一对键值对,由WithValue()函数产生

type valueCtx struct {
Context key, val interface{
}}

Context 实例化和派生

Context 实例化

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 。
WithCancel
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}}
WithDeadline
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()

WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))}

传递一个超时时间time.Duration,和 WithDeadline 用途一样。

WithValue
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 包的使用

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/

你可能感兴趣的文章
CCF 分蛋糕
查看>>
解决python2.7中UnicodeEncodeError
查看>>
小谈python 输出
查看>>
Django objects.all()、objects.get()与objects.filter()之间的区别介绍
查看>>
python:如何将excel文件转化成CSV格式
查看>>
Django 的Error: [Errno 10013]错误
查看>>
机器学习实战之决策树(一)
查看>>
[LeetCode By Python] 2 Add Two Number
查看>>
python 中的 if __name__=='__main__' 作用
查看>>
机器学习实战之决策树二
查看>>
[LeetCode By Python]7 Reverse Integer
查看>>
[LeetCode By Python]9. Palindrome Number
查看>>
[LeetCode By Python]13 Roman to Integer
查看>>
[leetCode By Python] 14. Longest Common Prefix
查看>>
[LeetCode By Python]107. Binary Tree Level Order Traversal II
查看>>
[LeetCode By Python]108. Convert Sorted Array to Binary Search Tree
查看>>
[leetCode By Python]111. Minimum Depth of Binary Tree
查看>>
[LeetCode By Python]112. Path Sum
查看>>
[LeetCode By Python]118. Pascal's Triangle
查看>>
[LeetCode By Python]119. Pascal's Triangle II
查看>>