概要
- 什么是Context
- 为什么要使用context
- Context基本的使用
- Context接口实现
- 使用场景
- 小结
1. 什么是Context
它用一句话来说就是: 控制goroutine的生命周期, 在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费。Context 通常被称之为上下文, 我们可以理解为: 一般理解为goroutine的一个运行状态、现场、快照。
1.1 为什么要使用context
在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作。
channeldone channel
done channel
func main() {
// 数据通道
messages := make(chan int, 10)
// 信号通道
done := make(chan bool)
defer close(messages)
// consumer
go func() {
// 每隔一秒执行一次,定时器
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
// 若关闭了通道则 执行下面的代码
case <-done:
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-messages)
}
}
}()
// producer
for i := 0; i < 10; i++ {
messages <- i
}
time.Sleep(5 * time.Second)
// 关闭通道, 退出上面的匿名函数
close(done)
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
如果我们可以在简单的通知上附加传递额外的信息来控制取消。
考虑下面这种情况:假如主协程中有多个任务1, 2, …m,主协程对这些任务有超时控制。
而其中任务1又有多个子任务1, 2, …n,任务1对这些子任务也有自己的超时控制,那么这些子任务既要感知主协程的取消信号,也需要感知任务1的取消信号。
done channeldone channel
我们需要一种优雅的方案来实现这样一种机制:
- 上层任务取消后,所有的下层任务都会被取消;
- 中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级任务。
context
2. context的使用
2.1 创建context两种方式
注意: 这两种方式是创建根context,不具备任何功能,需要用到with系列函数来实现具体功能
context.Backgroud()context.TODO()
2.2 两种方式比较
context.Backgroundcontext.TODO
2.3 With系列函数
context具体的用法 通过下面的几个具体demo来演示一下
2.3.1 context.withCancel() 取消控制
日常业务开发中我们往往为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致我们会在一次请求中开了多个goroutine确无法控制他们,这时我们就可以使用withCancel来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用cancel来进行取消。
// 案例一
/*
代码逻辑:
我们使用withCancel创建一个基于Background的ctx,然后启动一个讲话程序,
每隔1s说一话,main函数在10s后执行cancel,那么speak检测到取消信号就会退出。
*/
package main
import (
"context"
"fmt"
"time"
)
// context.Background()函数创建根上下文,返回父context和cancel函数
func NewWithCancel() (context.Context, context.CancelFunc) {
return context.WithCancel(context.Background())
}
// 业务逻辑
func Speak(ctx context.Context) {
for range time.Tick(time.Second) {
select {
case <-ctx.Done():
fmt.Println("关闭线程...")
fmt.Println(ctx.Err())
default:
fmt.Println("hahahhaa")
}
}
}
func main() {
// 创建父context和cancel函数
ctx, cancel := NewWithCancel()
// 使用协程来启动业务逻辑
go Speak(ctx)
time.Sleep(10 * time.Second)
// 取消的信号,结束Speak函数的运行
cancel()
time.Sleep(1 * time.Second)
}
// 案例二
/*
代码逻辑:
1. 利用根Context创建一个父Context,使用父Context创建一个协程,
2. 利用上面的父Context再创建一个子Context,使用该子Context创建一个协程
3. 一段时间后,调用父Context的cancel函数,会发现父Context的协程和子Context的协程都收到了信号,被结束了
*/
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 父context(利用根context得到)
ctx, cancel := context.WithCancel(context.Background())
// 父context的子协程
go watch1(ctx)
// 子context,注意:这里虽然也返回了cancel的函数对象,但是未使用
valueCtx, _ := context.WithCancel(ctx)
// 子context的子协程
go watch2(valueCtx)
fmt.Println("现在开始等待3秒,time=", time.Now().Unix())
time.Sleep(3 * time.Second)
// 调用cancel()
fmt.Println("等待3秒结束,调用cancel()函数")
cancel()
// 再等待5秒看输出,可以发现父context的子协程和子context的子协程都会被结束掉
time.Sleep(5 * time.Second)
fmt.Println("最终结束,time=", time.Now().Unix())
}
// 父context的协程
func watch1(ctx context.Context) {
for {
select {
case <-ctx.Done(): //取出值即说明是结束信号
fmt.Println("收到信号,父context的协程退出,time=", time.Now().Unix())
return
default:
fmt.Println("父context的协程监控中,time=", time.Now().Unix())
time.Sleep(1 * time.Second)
}
}
}
// 子context的协程
func watch2(ctx context.Context) {
for {
select {
case <-ctx.Done(): //取出值即说明是结束信号
fmt.Println("收到信号,子context的协程退出,time=", time.Now().Unix())
return
default:
fmt.Println("子context的协程监控中,time=", time.Now().Unix())
time.Sleep(1 * time.Second)
}
}
}
2.3.2 context.WithTimeout 超时控制
webrpcwithTimeoutwithDeadlinewithTimeoutwithDeadlineContextcancelFunccancelFunc
不同在于将持续时间作为参数输入而不是时间对象,这两个方法使用哪个都是一样的,看业务场景和个人习惯了,因为本质withTimout
// 案例一
package main
import (
"context"
"fmt"
"time"
)
// 创建一个带超时context, 三秒后退出执行
func NewContextWithTimeout() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 3*time.Second)
}
// 处理程序
func HttpHandler() {
ctx, cancel := NewContextWithTimeout()
defer cancel()
deal(ctx)
}
// 业务逻辑代码
func deal(ctx context.Context) {
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
}
}
}
func main() {
HttpHandler()
}
// 案例二
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创建一个子节点的context,3秒后自动超时
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
go watch(ctx, "监控1")
go watch(ctx, "监控2")
fmt.Println("现在开始等待8秒,time=", time.Now().Unix())
time.Sleep(8 * time.Second)
fmt.Println("等待8秒结束,准备调用cancel()函数,发现两个子协程已经结束了,time=", time.Now().Unix())
cancel()
}
// 单独的监控协程
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "收到信号,监控退出,time=", time.Now().Unix())
return
default:
fmt.Println(name, "goroutine监控中,time=", time.Now().Unix())
time.Sleep(1 * time.Second)
}
}
}
[谨慎使用]
trace_idtrace_idpythongevent.localjavaThreadLocalGoContextWithValuetrace_idcontext
/*
我们基于context.Background创建一个携带trace_id的ctx,然后通过context树一起传递,
从中派生的任何context都会获取此值,我们最后打印日志的时候就可以从ctx中取值输出到日志中。
目前一些RPC框架都是支持了Context,所以trace_id的向下传递就更方便了
*/
package main
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
)
type MyKEY string
const (
KEY MyKEY = "trace_id"
)
// 返回一个UUID
func NewRequestID1() MyKEY {
return MyKEY(strings.Replace(uuid.New().String(), "-", "", -1))
}
// 创建一个携带trace_id 的ctx
func NewContextWithTraceID() context.Context {
ctx := context.WithValue(context.Background(), KEY, NewRequestID1())
return ctx
}
// 打印值
func PrintLog(ctx context.Context, message string) {
fmt.Printf("%s|info|trace_id=%s|%s", time.Now().Format("2006-01-02 15:04:05"), GetContextValue1(ctx, KEY), message)
}
// 获取设置的key对应的值,并断言
func GetContextValue1(ctx context.Context, k MyKEY) MyKEY {
v, ok := ctx.Value(k).(MyKEY)
fmt.Println("打印k:" + k)
fmt.Printf("打印v: %v\n", v)
if !ok {
return ""
}
return v
}
func ProcessEnter(ctx context.Context) {
PrintLog(ctx, "Golang")
}
func main() {
ProcessEnter(NewContextWithTraceID())
}
几点建议:
- 不建议使用context值传递关键参数,关键参数应该显示的声明出来
- 因为携带value也是key value,避免context多个包使用带来的冲突,建议使用内置类型
- context传递的数据key value都是interface, 所以类型断言时别忘了保证程序的健壮性
context
type Context interface{
// 返回 context.Context 被取消的时间,也就是完成工作的截止日期;如果没有设定期限,将返回ok == false
Deadline()(deadline time.Time, ok bool)
// 当绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil
Done() <-chan struct{}
// 当Context被取消或者关闭后,返回context取消的原因
// 如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因
// 如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded
Err() error
// 获取设置的key对应的值
// 同时用于获取特定于当前任务树的额外信息
Value(key interface{}) interface{}
}
4. 应用场景
RPC调用PipeLine超时请求HTTP服务器的request互相传递数据
5. 小结
context.Context
context.Context