12-常用标准库之-context
一 Context是什么
1.1 介绍
Context,翻译为"上下文",context包定义了Context接口类型,其接口方法定义了跨API和进程之间的执行最后期限、取消信号和其他请求范围的值
在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作
context常用的使用场景:
- 一个请求对应多个goroutine之间的数据交互
- 超时控制
- 上下文控制
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中传递