go并发编程之context

英文原版context package - context - Go Packages

简介

context 上下文

上下文定义了上下文类型,它跨API边界和进程之间传递截止日期、取消信号和其他请求范围的值。对服务器的传入请求应创建上下文,对服务器的传出调用应接受上下文。。当上下文被取消时,从它派生的所有上下文也被取消(层层传递,级联取消)。未调用CancelFunc会泄漏子级及其子级,直到父级被取消或计时器触发。

使用上下文的程序应遵循以下规则

  • 以保持跨包的接口一致,并启用静态分析工具来检查上下文传播
  • 不要在结构类型内存储上下文;相反,将上下文显式传递给每个需要它的函数。

它应该在程序中流动,从一个函数传递到另一个函数,并根据需要进行扩展。理想情况下,每个请求开的时候都会创建一个 context 对象,层层传递,并在请求结束时结束。

  • 所有阻塞操作 及耗时较长操作都应该支持取消功能
  • 函数调用链使用上下文进行传播,在传播过程中可以选择将其替换为使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文
  • 上下文应该是第一个参数,通常命名为ctx:
func DoSomething(ctx context.Context, arg Arg) error {
	// ... 使用上下文 ...
}

即使函数允许,也不要传递nil上下文。如果您不确定要使用哪个上下文,请传递context.TODO。

上下文值仅用于传输进程和API的请求范围数据,不要用于将可选参数传递给函数。

相同的上下文可以传递给在不同goroutine中运行的函数;上下文对于多个goroutine并发是安全的。

funcWithCancel

级联取消

当一个 context 被取消时,从它派生的所有 context 也将被取消。WithCancel(ctx) 参数 ctx 认为是 parent ctx,在内部会进行一个传播关系链的关联。Done() 返回 一个 chan,当我们取消某个parent context, 实际上上会递归层层 cancel 掉自己的 child context 的 done chan 从而让整个调用链中所有监听 cancel 的 goroutine 退出。。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

取消此上下文将释放与之相关的资源,因此代码应该在该上下文中运行的操作完成后立即调用cancel。

如果要实现一个超时控制,通过上面的 context  parent/child 机制,其实我们只需要启动一个定时器,然后在超时的时候,直接将当前的 context 给 cancel 掉,就可以实现监听在当前和下层的额context.Done() 的 goroutine 的退出

例子一

此示例演示了如何使用context.WithCancel()来防止goroutine泄漏。在示例函数结束时,gen启动的goroutine将返回而不会泄漏。

package main

import (
	"context"
	"fmt"
)

func main() {
//gen在单独的goroutine中生成整数,并将它们发送到返回的通道。gen的调用者需要取消一次上下文它们消耗生成的整数以避免泄漏
//gen。
	gen := func(ctx context.Context) <-chan int {
		dst := make(chan int)
		n := 1
		go func() {
			for {
				select {
				case <-ctx.Done():
					return // 返回不会导致资源泄露
				case dst <- n:
					n++
				}
			}
		}()
		return dst
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 当我们停止遍历整数后进行取消

	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
	}
}

funcWithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithDeadline返回父上下文的副本,截止时间调整为不晚于d。如果父上下文的最后期限早于d,则WithDeadline(parent,d)在语义上等同于parent。当截止日期过期、调用返回的cancel函数或父上下文的Done通道关闭时(以先发生的为准),返回的上下文的Done通道关闭。取消此上下文将释放与之相关的资源,因此代码应该在该上下文中运行的操作完成后立即调用cancel。

例子

pack(
	"context"
	"fmt"
	"time"
)

const shortDuration = 1 * time.Millisecond

func main() {
	d := time.Now().Add(shortDuration)
	ctx, cancel := context.WithDeadline(context.Background(), d)
//尽管ctx将过期,但最好的做法是将取消放在所有情况里面比如说defer。否则可能会导致上下文及其父对象的生存时间超过了必要的时间。
	
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}

}

程序会输出 context deadline exceeded

func WithTimeout

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout返回WithDeadline(parent,time.Now().Add(timeout))

取消此上下文将释放与其相关的资源,因此代码应在该上下文中运行的操作完成后立即调用cancel:

如果慢操作在超时之前完成,则释放资源

func slowOperationWithTimeout(ctx context.Context) (Result, error) {
	ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
	defer cancel()  // 
	return slowOperation(ctx)
}

例子

func main() {
  //传递一个带有超时控制的上下文,告诉阻塞函数它应在超时后放弃其工作。
  ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
  defer cancel()

  select {
  case <-time.After(1 * time.Second):
    fmt.Println("overslept")
  case <-ctx.Done():
    fmt.Println(ctx.Err()) // 打印 "context deadline exceeded"
  }

}

func Background() Context

background函数返回非零的空上下文。它不能被直接取消,没有值,也没有过期时间。它通常由主函数、初始化和测试使用,并作为传入请求的根上下文。

func TODO()Context

TODO返回一个非零的空上下文。代码应该使用上下文。当不清楚要使用哪个上下文或它还不可用时(因为周围的函数还没有扩展到接受上下文参数)。

func WithValue(parent Context, key, val anyContext

WithValue返回父级上下文的副本,其中与键关联的值为val。上下文值仅用于传输进程和API的请求范围数据(比如说链路追踪id),而不用于将可选参数传递给函数。提供的键必须具有可比性,值数据必须是并发安全的,因为某些场景会并发读写,以避免使用上下文的包之间发生数据竞争。

注意点

context value 应该是 不变 的,每次重新赋值应该是新的 context,即: context.WithValue(ctx, oldvalue)

例子

ackage main

import (
	"context"
	"fmt"
)

func main() {
	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.Background() 的 ctx1,携带了一个 map 的数据,map 中包含了 “k1”: “v1” 的一个键值对,ctx1 被两个 goroutine 同时使用作为函数签名传入,如果我们修改了 这个map,会导致另外进行读 context.Value 的 goroutine 和修改 map 的 goroutine,在 map 对象上产生 data race。因此我们要使用 copy-on-write 的思路,解决跨多个 goroutine 使用数据、修改数据的场景。

copy-on-write 从 ctx1 中获取 map1(可以理解为 v1 版本的 map 数据)。构建一个新的 map 对象 map2,复制所有 map1 数据,同时追加新的数据 “k2”: “v2” 键值对,使用 context.WithValue 创建新的 ctx2ctx2 会传递到其他的 goroutine 中。这样各自读取的副本都是自己的数据,写行为追加的数据,在 ctx2 中也能完整读取到,同时也不会污染 ctx1 中的数据。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值