Go语言基础(五) context

一、context

假如有个请求HTTP服务,那么由一个go routine 处理;服务要访问DB,会启动另一个go routine处理。请求要求服务rt 在 500ms 内,否则返回失败,但此时处理 DB 的go routine还在干活 — 继续干活也没意义了,需要“中断”或“结束”这个 go routine ,如何实现呢?

这就引申了今天的话题:

1.1 如何优雅地结束一个go routine ?

如何优雅地停止一个 goroutine 呢?这里给出两个方法。

//  方法一:
// 下面这种定义 全局的bool值的办法是一种思路,不过 go 提供了更加优雅的方案
var wg sync.WaitGroup
var notify bool= false
func work(){
	defer wg.Done()
	for{
		fmt.Println("working...")
		time.Sleep(time.Second)
		if notify {
			break
		}
	}
}
func main() {
	wg.Add(1)
	go work()
	time.Sleep(time.Second * 5)
	notify = true
	wg.Wait()
	fmt.Println("main stops")
}
// 方法二:使用通道
var ch = make(chan struct{})
var wg sync.WaitGroup

func work() {
	defer wg.Done()
Loop:
	for {
		fmt.Println("working...")
		time.Sleep(time.Second)
		select {
		case <-ch:
			//这里是希望能够 中断整个 for 循环,如果只是简单的break就只能跳出 select case
			break Loop
		default:

		}

	}
}
func main() {
	wg.Add(1)
	go work()
	time.Sleep(time.Second*5)
	ch <- struct{}{}
	wg.Wait()
	fmt.Println("main go routine stops")
}

这两种方法功能上OK,但这是由用户自己实现的,实现难统一。再看官方给的方案:

var wg sync.WaitGroup

func work(ctx context.Context) {
	wg.Done()
Loop:
	for {
		fmt.Println("working")
		time.Sleep(time.Second)

		select {
		case <-ctx.Done():
			break Loop
		default:
		}
	}
}

func main() {
	ctx, cancelFunc := context.WithCancel(context.Background())
	wg.Add(1)
	go work(ctx)
	time.Sleep(time.Second * 5)
	cancelFunc()
	wg.Wait()
}

就这个例子而言,官方的方案和原先的方法二本质是相同的。抽象出context类库之后,统一了实现。

1.2 context

context 场景: 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用WithCancelWithDeadlineWithTimeoutWithValue创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。

【context 是处理 多个routine 之间“联动”的大杀器。java中就没有类似的抽象】

1.3 Background、TODO

context是个接口,Background TODO是 实现该接口的结构体。一般我们以Background or TODO 作为顶层的 parent context,衍生出更多的子上下文对象

  • Background:用于main函数、初始化、测试代码;作为context树结构的最顶层的context,也就是根context
  • TODO:表示目前还不知道具体的场景。假如当前不知道要使用什么context,就使用 TODO好了。

本质上,Background、TODO都是emptyCtx类型,是一个不可取消、无截止时间、不带任何值的Context

1.4 with 函数

  • func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    看官方解释:
    WithCancel returns a copy of parent with a new Done channel. The returned context's Done channel is closed when the returned cancel function is called or when the parent context's Done channel is closed, whichever happens first. Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.

  • withDeadline

  • withTimeout

  • withvalue

先看第一个withCancel的例子:

需求:一个go routine 不断产生 自然数,但假如 main routine 退出了 ,这个go routine 也退出。


func main() {
	ctx,cancelFunc := context.WithCancel(context.Background())

	defer cancelFunc()

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 10 {
			break
		}
	}
}
//返回只读的channel
func gen(ctx context.Context) <-chan int{
	result:=make(chan int)
	n := 1
	go func() {
		for{
			select {
			case <- ctx.Done():
				return	// in case of  go routine leak
			case result <- n:
				n++
			}
		}
	}()
	return result
}

WithDeadline的例子:

func main() {
	d := time.Now().Add(50 * time.Microsecond) //设置了 50 ms 过期
	ctx, cancelFunc := context.WithDeadline(context.Background(), d)

	defer cancelFunc()

	// for{
		select {
			// main routine 会阻塞在此处等 1s ,但由于 withDeadline()超时设置了 50ms,所以 Done() 先关闭
		case <-time.After(time.Second):	
			fmt.Println("after....")
		case <-ctx.Done():
			fmt.Println(ctx.Err())  
		}
	// }
}

withTimeout的例子:


var wg sync.WaitGroup

func main() {
	// 50 ms 超时
	ctx,cancelFunc:=context.WithTimeout(context.Background(), 50 * time.Microsecond)
	wg.Add(1)
	go func (context context.Context)  {
		Loop:
		for{
			fmt.Println("fake connecting to db") // 10ms 超时
			time.Sleep(time.Millisecond * 10)
			select {
			case <- context.Done():	// 会阻塞在此
				break Loop
			default:
			}
		}
		wg.Done()
	}(ctx)

	time.Sleep(5 * time.Second)
	cancelFunc()
	wg.Wait()

	fmt.Println("main routine exits")
}

withValue的案例:

type  TraceCode string
var wg sync.WaitGroup
func main() {
	ctx, cancelFunc := context.WithTimeout(context.Background(), time.Millisecond*50)
	ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"),"12121212")
	wg.Add(1)
	go worker(ctx)
	time.Sleep(time.Second * 5)
	cancelFunc()
	wg.Wait()
	fmt.Println("main routine done")
}

func worker(ctx context.Context){
	key:= TraceCode("TRACE_CODE")
	// 断言 traceCode 一定是一个 字符串
	traceCode,ok:=ctx.Value(key).(string)
	if !ok {
		fmt.Println("illegal trace code")
	}

	Loop:
	for{
		fmt.Printf("trace code %s \n", traceCode)
		time.Sleep(time.Millisecond * 10)
		select {
		case <-ctx.Done():
			break Loop
		default:
		}
	}

	fmt.Println("worker done")
	wg.Done()

}

1.5 Context的最佳实践

  • 推荐以参数的方式显示传递Context
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context是线程安全的,可以放心的在多个goroutine中传递

二、

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值