在Go
服务器中,每个传入的请求都在其自己的goroutine
中处理。
请求处理程序通常会启动其他goroutine
来访问后端,例如数据库和RPC服务
处理请求的goroutine
集合通常需要访问特定于请求的值,例如最终用户的身份,授权令牌和请求的截止日期。
当请求被取消或超时时,处理该请求的所有goroutine
都应该快速退出,以便系统可以回收它们正在使用的任何资源。
go提供了一个'context'包,也就是上下文,来达到控制他们的目的,可以轻松地将请求范围的值,取消信号和API
边界的截止日期传递给处理请求所涉及的所有goroutine
。
context的使用
在请求处理的过程中,会调用各层的函数,每层的函数会创建自己的routine,构成了一个routine树, 如果由Context来控制上下文,context也应该反映并实现成一棵树
![](https://img.haomeiwen.com/i15203565/46f56975d8bd7483.jpg)
根节点
要创建context树,第一步是要有一个根结点。context.Background函数的返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个routine创建,不能被取消、没有值、也没有过期时间
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
-
子节点
context包为我们提供了以下函数,当一个context取消,所有从他衍生的contexts都会被取消 -
context的带的方法
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Deadline: 返回1个time.time,表示当前context应该结束的时间,ok表示有结束时间
- Done: context被取消或超时返回close的chan,告诉context相关的函数停止当前工作返回(类似全局广播)
- Err: context被取消的原因
- Value: context实现共享数据的地方,是协程安全的
Context 所以方法
func Background() Context
func TODO() Context
// ctx, cancel := context.WithCancel(Background())
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(5 * time.Second)) 5秒之后取消
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
// ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 10秒后取消
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// ctxVal := context.WithValue(ctx, "hello", "world")
func WithValue(parent Context, key interface{}, val interface{}) Context
这四个函数的第一个参数都是父context,返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收的函数参数保存子节点的一些状态值,然后就可以将它传递给下层的routine了
例子
- 例一:WithCancel()
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 用chan实现锁
c := make(chan bool, 1)
go worker(ctx, "worker1", c)
go worker(ctx, "worker2", c)
go worker(ctx, "worker3", c)
time.Sleep(5 * time.Second)
fmt.Println("通知工作停止")
cancel()
for i := 0; i < 3; i++ {
// 接收到3次数据,表明3个worker都关闭
// 如果没有这个锁,可能cancel()执行后,协程可能没来得及退出,主函数就结束了
// 比sync.waitGroup好用,wait跟context搭配容易出奇奇怪怪的问题
<-c
}
close(c)
fmt.Println("所有工作停止")
}
func worker(ctx context.Context, workerId string, c chan bool) {
for {
select {
case <-ctx.Done():
fmt.Println(workerId, "goroutine stop worked")
c <- true
return
default:
fmt.Println(workerId, "goroutine is working...")
time.Sleep(1500 * time.Millisecond)
}
}
}
- 例二:WithDeadline
func main() {
// 截至某某时间,自动取消,也可以用cancel提前取消
// 当前时间的5秒后,自动取消
t := time.Now().Add(5 * time.Second)
ctx, _ := context.WithDeadline(context.Background(), t)
c := make(chan bool, 1)
go worker(ctx, "worker1", c)
go worker(ctx, "worker2", c)
go worker(ctx, "worker3", c)
for i := 0; i < 3; i++ {
<-c
}
close(c)
fmt.Println("所有工作停止")
}
func worker(ctx context.Context, workerId string, c chan bool) {
for {
select {
case <-ctx.Done():
fmt.Println(workerId, "goroutine stop worked")
c <- true
return
default:
fmt.Println(workerId, "goroutine is working...")
time.Sleep(1500 * time.Millisecond)
}
}
}
- 例三:WithTimeout
func main() {
// cancel可以忽略,5秒后自动取消
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
c := make(chan bool, 1)
go worker(ctx, "worker1", c)
go worker(ctx, "worker2", c)
go worker(ctx, "worker3", c)
for i := 0; i < 3; i++ {
<-c
}
close(c)
fmt.Println("所有工作停止")
}
func worker(ctx context.Context, workerId string, c chan bool) {
for {
select {
case <-ctx.Done():
fmt.Println(workerId, "goroutine stop worked")
c <- true
return
default:
fmt.Println(workerId, "goroutine is working...")
time.Sleep(1500 * time.Millisecond)
}
}
}
- 例四:WithValue
func main() {
ctx, cancel := context.WithCancel(context.Background())
val := "hello world"
// 由于withValue没有返回cancel,只能作为有cancel节点的子节点
ctxVal := context.WithValue(ctx, "worker", val)
c := make(chan bool, 1)
go worker(ctxVal, c)
go worker(ctxVal, c)
go worker(ctxVal, c)
time.Sleep(5 * time.Second)
fmt.Println("通知所有工作准备停止")
cancel()
for i := 0; i < 3; i++ {
<-c
}
close(c)
fmt.Println("所有工作已经停止")
}
func worker(ctx context.Context, c chan bool) {
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Value("worker"), "goroutine stop worked")
c <- true
return
default:
fmt.Println(ctx.Value("worker"), "goroutine is working...")
time.Sleep(1500 * time.Millisecond)
}
}
}
总结
context包是用来管理进程以及衍生出的协程。打一个比方,一个项目三组人,项目相当于进程,每一组相对于一个线程,组里面每个人相当于协程。
当一个组任务完成了,组员们可以停下你们的工作了。那么现实中就是需要,组长告知大家,所有人停止任务。同样协程也需要,一个信号告诉他们停止任务。
context包就起到通知的功能。项目进程持有一个顶级A Context,每个组持有A Context衍生的B,C,D Context,组成员持有组衍生X,Y,Z Context.
Context 使用原则
不要把Context放在结构体中,要以参数的方式传递
以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
Context是线程安全的,可以放心的在多个goroutine中传递
网友评论