12-常用标准库之-context

一 Context是什么

1.1 介绍

Context,翻译为"上下文",context包定义了Context接口类型,其接口方法定义了跨API和进程之间的执行最后期限、取消信号和其他请求范围的值

在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作
context常用的使用场景:
  1. 一个请求对应多个goroutine之间的数据交互
  2. 超时控制
  3. 上下文控制

1.2 Context接口方法

context.Context是一个接口,该接口定义了四个需要实现的方法。具体如下:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

其中:

  • Deadline:是获取设置的截止时间的意思,第一个返回值是截止时间,到了这个时间点,Context 会自动发起取消请求;第二个返回值 ok==false 时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
  • Done:该方法返回一个只读的 chan,类型为 struct{},我们在 goroutine 中,如果该方法返回的 chan 可以读取,则意味着parent context已经发起了取消请求,我们通过 Done 方法收到这个信号后,就应该做清理操作,然后退出 goroutine,释放资源。
  • Err 方法返回取消的错误原因,因为什么 Context 被取消。
  • Value方法获取该 Context 上绑定的值,是一个键值对,所以要通过一个 Key 才可以获取对应的值,这个值一般是线程安全的

1.3 两个顶级Context

context包提供两种顶级的上下文类型,这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象:

func Background() Context

context.Background()返回非零的空上下文。它从不被取消,没有值,也没有最后期限。它通常由主函数、初始化和测试使用,并且作为传入请求的顶级上下文。

func TODO() Context

context.TODO()返回非零的空上下文。当不清楚要使用哪个上下文或者它还不可用时(,应该使用context.TODO()。

两者区别

本质来讲两者区别不大,其源码实现是一样的,只不过使用场景不同,context.Background()通常由主函数、初始化和测试使用,是顶级Context;context.TODO()通常用于主协程外的其他协程向下传递,分析工具可识别它在调用栈中传播

1.4 派生Context(With系列函数)

除以上两种顶级Context类型,context包提供四种创建可派生Context类型的函数

WithCancel

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

WithCancel函数返回具有新done通道的父级副本。当调用返回的cancel函数或关闭父上下文的done通道时(以先发生者为准),将关闭返回的上下文的done通道。
取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。

示例通过context控制多个协程停止:

package main

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

func task(ctx context.Context, s string) {
lqz:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我结束了")
			break lqz  // 结束掉 label位置的循环
		default:
			fmt.Println("打印一次传入的值:", s)
			time.Sleep(1 * time.Second)

		}
	}
}
func main() {
	parent := context.Background()
	ctx, cancle := context.WithCancel(parent)
	go task(ctx, "lqz is Nb")
	go task(ctx, "lqz is handsome")
	time.Sleep(5 * time.Second) // 睡个5s钟,发现上面两句话不停打印
	cancle()  // 通过ctx控制,上面两个go协程关闭
	time.Sleep(5 * time.Second)  // 睡个5s钟,发现确实被停止了,不打印了

}

WithDeadline

 func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
// 注意第二个参数是时间对象

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

官方使用示例:
这个例子传递一个具有任意截止时间的上下文,告诉一个阻塞函数一旦到达它就应该放弃它的工作。

package main

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

func task(ctx context.Context, s string) {
lqz:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我结束了")
			fmt.Println(ctx.Err())
			// 正常到时间:context deadline exceeded
			// 手动调用cancel :context canceled

			break lqz // 结束掉 label位置的循环

		default:
			fmt.Println("打印一次传入的值:", s)
			time.Sleep(1 * time.Second)

		}
	}
}
func task2(ctx context.Context, s string) {
lqz:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("task:我收到取消指令,我结束了")
			fmt.Println(ctx.Err())
			// 正常到时间:context deadline exceeded
			// 手动调用cancel :context canceled

			break lqz // 结束掉 label位置的循环
		case <-time.After(1 * time.Second):
			fmt.Println("1s时间到了,打印:",s)
			fmt.Println(ctx.Err()) // 执行到此,如果还没到结束时间,Err为nil
		}
	}
}
func main() {
	// 1 正常到时间
	//parent := context.Background()
	//t:=time.Now().Add(5*time.Second) // 5s后的时间
	//ctx, _ := context.WithDeadline(parent,t)
	//go task(ctx, "lqz is Nb")
	//time.Sleep(10 * time.Second) // 睡个10s钟,由于5s结束,后5s没有输出

	// 2 手动调用cancle取消
	//parent := context.Background()
	//t := time.Now().Add(5 * time.Second) // 5s后的时间
	//ctx, cancel := context.WithDeadline(parent, t)
	//go task(ctx, "lqz is Nb")
	//time.Sleep(3 * time.Second) // 睡个3s钟,由于5s还没到,手动结束
	//cancel()
	//time.Sleep(7 * time.Second) // 再睡7s看输出

	//3 1s后输出一次内容的另一种写法
	parent := context.Background()
	t := time.Now().Add(5 * time.Second) // 5s后的时间
	ctx, cancel := context.WithDeadline(parent, t)
	go task2(ctx, "lqz is Nb")
	time.Sleep(3 * time.Second) // 睡个3s钟,由于5s还没到,手动结束
	cancel()
	time.Sleep(7 * time.Second) // 再睡7s看输出


}


WithTimeout

 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 注意第二个参数是time.Duration 时间间隔

WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消:

这个例子传递一个带有超时的上下文,告诉一个阻塞函数它应该在超时结束后放弃它的工作。

package main

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

func task(ctx context.Context) {
	select {
	case <-ctx.Done():
		fmt.Println("task:我结束了")
		// cancle函数取消会打印context canceled
		// 到时间取消会打印:context deadline exceeded
		fmt.Println(ctx.Err())
	case <-time.After(1 * time.Second):
		fmt.Println("1s时间到了")
		fmt.Println(ctx.Err()) // 执行到此,如果还没到结束时间,Err为nil

	}

}

func main() {
	//ctx, cancle := context.WithTimeout(context.Background(), 1*time.Second) // 打印
	ctx, cancle := context.WithTimeout(context.Background(), 2*time.Second)
	go task(ctx)

	time.Sleep(3*time.Second)
	cancle()
	time.Sleep(3*time.Second)


}

WithValue

 func WithValue(parent Context, key, val interface{}) Context

WithValue返回父级的副本,可为上下文设置一个键值对。
只对传输进程和API的请求范围数据使用上下文值,而不用于向函数传递可选参数。
提供的键必须是可比较的,并且不应是字符串或任何其他内置类型,以避免使用上下文的包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给接口时进行分配,上下文键通常具有具体的类型结构。或者,导出的上下文键变量的静态类型应该是指针或接口。

示例:

package main

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

func task(ctx context.Context) {
	fmt.Println(ctx.Value("name"))
	select {
	case <-ctx.Done():
		fmt.Println("task:我结束了")
		fmt.Println(ctx.Err())
	case <-time.After(1 * time.Second):
		fmt.Println("1s时间到了")
		fmt.Println(ctx.Err())

	}

}

func main() {
	ctx, _ := context.WithTimeout(context.Background(), 1*time.Second) // 1s超时的ctx
	ctx = context.WithValue(ctx, "name", "lqz")
	go task(ctx)
	time.Sleep(5*time.Second)
}

二 Context使用示例

2.1 控制10s后,所有协程退出

使用context包来实现线程安全退出或超时的控制:控制10s后,所有协程退出

package main

import (
	"context"
	"fmt"
	"strconv"
	"sync"
	"time"
)

func task(ctx context.Context, s string, wg *sync.WaitGroup) {
	defer wg.Done()
	for {
		select {
		case <-ctx.Done():
			fmt.Println(s, "--->我结束了")
			//fmt.Println(ctx.Err())
			return
		default:
			fmt.Println(s)
			time.Sleep(1 * time.Second)

		}
	}

}

func main() {
	var wg sync.WaitGroup
	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		s := fmt.Sprintf("我是第:%v 个任务", strconv.Itoa(i))
		go task(ctx, s, &wg)
	}
	wg.Wait()

}

当并发体超时或main主动停止工作者Goroutine时,每个工作者都可以安全退出。

2.2 控制某个go协程执行5次就结束

// 控制goroutine 执行5次结束
func main() {
	// 定义一个运行次数变量
	runCount := 0
	//定义一个waitgroup,等待goroutine执行完成
	var wg sync.WaitGroup
	// 初始化context
	parent := context.Background()
	// 传入初始化的ctx,返回ctx和cancle函数
	ctx, cancle := context.WithCancel(parent)
	wg.Add(1) // 增加一个任务
	go func() {
		for {
			select {
			case <-ctx.Done():
				fmt.Println("任务结束")
				return
			default:
				fmt.Printf("任务执行了%d次\n", runCount)
				runCount++
			}
			// 执行了5次,使用ctx的取消函数将任务取消
			if runCount >= 5 {
				cancle()
				wg.Done() // goroutine执行完成
			}

		}
	}()

	wg.Wait() //等待所有任务完成

}

2.3 打印100个素数

Go语言是带内存自动回收特性的,因此内存一般不会泄漏。当main函数不再使用管道时后台Goroutine有泄漏的风险。我们可以通过context包来避免这个问题,下面是防止内存泄露的素数筛实现:

// 返回生成自然数序列的管道: 2, 3, 4, ...
func GenerateNatural(ctx context.Context) chan int {
    ch := make(chan int)
    go func() {
        for i := 2; ; i++ {
            select {
            //父协程cancel()时安全退出该子协程
            case <- ctx.Done():
                return
            //生成的素数发送到管道
            case ch <- i:
            }
        }
    }()
    return ch
}

// 管道过滤器: 删除能被素数整除的数
func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
    out := make(chan int)
    go func() {
        for {
            if i := <-in; i%prime != 0 {
                select {
                //父协程cancel()时安全退出该子协程
                case <- ctx.Done():
                    return
                case out <- i:
                }
            }
        }
    }()
    return out
}

func main() {
    // 使用一个可由父协程控制子协程安全退出的Context。
    ctx, cancel := context.WithCancel(context.Background())

    ch := GenerateNatural(ctx) // 自然数序列: 2, 3, 4, ...
    
    for i := 0; i < 100; i++ {
        // 新出现的素数打印出来
        prime := <-ch 
        fmt.Printf("%v: %v\n", i+1, prime)
        // 基于新素数构造的过滤器
        ch = PrimeFilter(ctx, ch, prime) 
    }
    
    //输出100以内符合要求的素数后安全退出所有子协程
    cancel()
}

当main函数完成工作前,通过调用cancel()来通知后台Goroutine退出,这样就避免了Goroutine的泄漏。

三 使用Context的注意事项

  • 推荐以参数的方式显示传递Context
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context是线程安全的,可以放心的在多个goroutine中传递
posted @ 2022-03-12 01:39  刘清政  阅读(840)  评论(0编辑  收藏  举报