golang的context

1.context简介

在go的http包的server中,每一个对应的请求都有一个goroutine负责处理,处理函数通常会启动额外的goroutine去处理,如果一个请求被取消或者超时,用来处理该请求的goroutine应该及时退出,这样就不会有大量的goroutine去占用资源。

Context类型专用来简化对于处理单个请求的多个goroutine之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个API调用。因此对服务器的请求应该去创建上下文,对服务器的传输调用也应该接收上下文,它们的函数调用必须传上下文,或可以使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文。当一个上下文取消时 它派生的所有上下文也会取消。
context.Context是一个接口,接口定义了四个实现的方法:
 

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  1. Deadline:返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。
  2. Done:返回一个只读的通道(只有在被cancel后才会返回),类型为 struct{}。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。 多次调用Done方法会返回同一个Channel。
  3. Err:返回 context 被 cancel 的原因。它只会在Done返回的Channel关闭时才会返回非空的值,如果当前Context被取消就会返回Canceled,如果当前Context超时就会返回DeadlineExceeded。
  4. Value:返回被绑定到 Context 的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。对于同一个上下文来 说,多次调用Value 并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间请求域的数据。

2.context解决的问题

当有多个goroutine在运行的时候,主goroutine如果提前结束,会导致其余的goroutine没有执行完毕,我们一般来用互斥锁+全局变量或者channel来解决。如下:

var wg sync.WaitGroup
var exit bool
// 全局变 方式存在的   
// 1. 使用全局变量在跨包调用时不容易统一
// 2. 如果worker中再启动goroutine 就不太好控制了。
func worker() {
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		if exit {
			break
		}
	}
	wg.Done()
}
func main() {
	wg.Add(1)
	go worker()
	time.Sleep(time.Second * 3) 
	exit = true 
	wg.Wait()
	fmt.Println("over")
}
func main() {
	stop := make(chan bool)
	go func() {
		for {
			select {
			case <-stop:
				fmt.Println("监控退出,停止了...")
				return
			default:
				fmt.Println("goroutine监控中...")
				time.Sleep(2 * time.Second)
			}
		}
	}()
	time.Sleep(10 * time.Second)
	fmt.Println("可以了,通知监控停止")
	stop<- true
	//为了检测监控过是否停止,如果没有监控输出,就表示停止了
	close(stop)
	time.Sleep(5 * time.Second)
}

在后台goroutine中,使用select判断stop是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行default里的监控逻辑,继续监控,只到收到stop的通知。

如果是多个 goroutine 一起运行,chan+select的方式,是比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,如果有很多goroutine都需要控制结束怎么办呢?如果这些goroutine又衍生了其他更多的goroutine怎么办呢?如果一层层的无穷尽的goroutine呢?依靠chan+select的方式这就非常复杂了,即使我们定义很多chan也很难解决这个问题,因为goroutine的关系链就导致了这种场景非常复杂。

func monitor(ctx context.Context, number int)  {
	for {
		select {
		// 其实可以写成 case <- ctx.Done()
		// 这里仅是为了让你看到 Done 返回的内容
		case v :=<- ctx.Done():
			fmt.Printf("监控器%v,接收到通道值为:%v,监控结束。\n", number,v)
			return
		default:
			fmt.Printf("监控器%v,正在监控中...\n", number)
			time.Sleep(2 * time.Second)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	for i :=1 ; i <= 5; i++ {
		go monitor(ctx, i)
	}
	time.Sleep( 1 * time.Second)
	// 关闭所有 goroutine 取消 context 
	cancel()
	// 等待5s,若此时屏幕没有输出 <正在监控中> 就说明所有的goroutine都已经关闭
	time.Sleep( 5 * time.Second)
	fmt.Println("主程序退出!!")
}

3.Background()和TODO()

Go内置两个函数Background()和TODO()两个函数分别返回一个实现了Context接口的background和todo。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context去衍生出更多的子上下文对象。

Background()主用于main函数、初始化以及测代码中 作为Context 的树结构的最顶层的Context 也就是根Context。

TODO() 它目前不知道具体的使用场景 如果我们不知道使用什么Context的时候 可以使用这个。background和todo本质上 是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

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
}

4.With系列函数

 context包中还定义了四个With系列函数。

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

这四个函数有一个共同的特点,就是第一个参数,都是接收一个父context。通过一次继承,就多实现了一个功能,比如使用 WithCancel 函数传入 根context ,就创建出了一个子 context,该子context 相比 父context,就多了一个 cancel context 的功能。

1.WithCancel

功能:返回一个继承的Context,在父协程context的Done函数被关闭时会关闭自己的Done通道,或者在执行了如下cancel函数之后,会关闭自己的Done通道。这种关闭的通道可以作为一种广播的通知操作,告诉所有context相关的函数停止当前的工作直接返回。通常使用场景用于主协程用于控制子协程的退出,用于一对多处理。
 

package main
 
import (
	"context"
	"fmt"
	"reflect"
	"time"
)
 
func main() {
 
	// 控制子协程安全的退出,调用cancle后,会关闭自己的通道,表示程序结束,所有子协程会安全的退出
	ctx, cancle := context.WithCancel(context.Background())
 
	defer cancle() // 取消函数上下文
 
	go func() {
		for {
			select {
            // ctx为一个接口类型,存储的就是一个cancelCtx结构的地址,所以,表面看起来就是一个值传递,实质上就是地址,接口接受很好表现了封装完整性
			case <-ctx.Done():
				return
			default:
				fmt.Println("go first ", reflect.TypeOf(ctx).Elem().Name())
			}
 
			time.Sleep(time.Second)
		}
	}()
 
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			default:
				fmt.Println("go second ", reflect.TypeOf(ctx).Elem().Name())
			}
			time.Sleep(time.Second)
		}
	}()
 
	go func() {
		for {
			select {
			case <-ctx.Done():
				return
			default:
				fmt.Println("go third ", reflect.TypeOf(ctx).Elem().Name())
			}
			time.Sleep(time.Second)
		}
	}()
 
	fmt.Println("main-",reflect.TypeOf(ctx).Elem())
	time.Sleep(5 * time.Second)
}

2.WithDeadline

功能:传递一个上下文,等待超时时间,超时后,会返回超时时间,并且会关闭context的Done通道,其他传递的context,收到Done关闭的消息的,直接返回即可。同样用户通知消息出来。

package main

import (
    "context"
    "fmt"
    "time"
)

func monitor(ctx context.Context, number int)  {
    for {
        select {
        case <- ctx.Done():
            fmt.Printf("监控器%v,监控结束。\n", number)
            return
        default:
            fmt.Printf("监控器%v,正在监控中...\n", number)
            time.Sleep(2 * time.Second)
        }
    }
}

func main() {
    ctx01, cancel := context.WithCancel(context.Background())
    ctx02, cancel := context.WithDeadline(ctx01, time.Now().Add(1 * time.Second))

    defer cancel()

    for i :=1 ; i <= 5; i++ {
        go monitor(ctx02, i)
    }

    time.Sleep(5  * time.Second)
    if ctx02.Err() != nil {
        fmt.Println("监控器取消的原因: ", ctx02.Err())
    }

    fmt.Println("主程序退出!!")
}

3.WithTimeout

功能:传递一个上下文,并且设置对应的超时时间,调用Deadline()判断当前上下文是否超时,同样用于通知消息进行处理,控制上下文的处理。

package main
 
import (
	"context"
	"log"
	"time"
)
 
func main() {
 
	// 定义一个超时上下文,指定相应的超时时间
	ctx, cancle := context.WithTimeout(context.Background(), 5*time.Second)
 
	defer cancle()
	go func() {
		for {
 
			time.Sleep(1 * time.Second)
			// 检查ctx何时会超时
			if deadline, ok := ctx.Deadline(); ok {
				log.Print("deadline !", deadline)
 
				// 判断当前时间是不是在ctx取消之后,直接终止该函数,此处判断超时空取消了ctx,可以直接退出返回.
				if time.Now().After(deadline) {
					log.Printf(ctx.Err().Error())
					return
				}
 
			}
			select {
			case <-ctx.Done():
				log.Print("done !")
                // return // 没有上面推出,可在此处退出函数
			default:
				log.Print("son !!!")
 
			}
		}
 
	}()
 
	time.Sleep(8 * time.Second)
}

4.WithValue

功能:用户传递上下文的消息信息,将需要传递的消息从一个协程传递到另外协程,引领上下文进行相关业务处理。

package main
 
import (
	"context"
	"fmt"
	"time"
)
 
func main() {
	ctx := context.WithValue(context.Background(), "name", "eric")
	ctx = context.WithValue(ctx, "session", 100001)
 
	go func(ctx *context.Context) {
		fmt.Println("start to go process")
		// session
		session, ok := (*ctx).Value("session").(int)
		if ok {
			fmt.Println(ok, "+", session)
		}
 
		name, ok := (*ctx).Value("name").(string)
		if ok {
			fmt.Println(ok, "+", name)
		}
 
		fmt.Println("end to go process")
 
	}(&ctx)
 
	// 让主协助程序等待子协程退出后,主协程在推出即可
	time.Sleep(time.Second)
 
}

5.注意事项

  1. 通常 Context 都是做为函数的第一个参数进行传递(规范性做法),并且变量名建议统一叫 ctx
  2. Context 是线程安全的,可以放心地在多个 goroutine 中使用。
  3. 当你把 Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到 取消的信号
  4. 不要把原本可以由函数参数来传递的变量,交给 Context 的 Value 来传递。
  5. 当一个函数需要接收一个 Context 时,但是此时你还不知道要传递什么 Context 时,可以先用 context.TODO 来代替,而不要选择传递一个 nil。
  6. 当一个 Context 被 cancel 时,继承自该 Context 的所有 子 Context 都会被 cancel。
     
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值