Golang理解-Context包
朦胧的朦组词为什么需要context
先举个例⼦:
在 Go http包的Server中,每⼀个请求在都有⼀个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine ⽤来访问后端服务,⽐如数据库和RPC服务。⽤来处理⼀个请求的 goroutine 通常需要访问⼀些与请求特定的数据,⽐如终端⽤户的⾝份认证信息、验证相关的token、请求的截⽌时间。当⼀个请求被取消或超时时,所有⽤来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占⽤的资源。
对于多并发的情况下,传统的⽅案:等待组sync.WaitGroup以及通过通道channel的⽅式的问题就会显现出来;
对于等待组控制多并发的情况:只有所有的goroutine都结束了才算结束,只要有⼀个goroutine没有结束,那么就会⼀直等,这显然对资源的释放是缓慢的;
⽽对于通道Channel的⽅式下:通过在main goroutine中像chan中发送关闭停⽌指令,并配合lect,从⽽达到关闭goroutine的⽬的,这种⽅式显然⽐等待组优雅的多,但是在goroutine中在嵌套goroutine的情
况就变得异常复杂。
等待组例⼦:
package main
import (
"fmt"
"sync"
"time"
"strconv"
)
var wg sync.WaitGroup
func run(task string) {
fmt.Println(task, "start。。。")
time.Sleep(time.Second * 2)
// 每个goroutine运⾏完毕后就释放等待组的计数器
wg.Done()
}
func main() {
wg.Add(2) // 需要开启⼏个goroutine就给等待组的计数器赋值为多少,这⾥为2汉蒙翻译
for i := 1; i < 3; i++ {
taskName := "task" + strconv.Itoa(i)
go run(taskName)
}
/
/ 等待,等待所有的任务都释放
wg.Wait()
fmt.Println("所有任务结束。。。")
}
输出结果:
task2 start。。。
task1 start。。。我是一个幸运的人
所有任务结束。。。
上⾯例⼦中,⼀个任务结束了必须等待另外⼀个任务也结束了才算全部结束了,先完成的必须等待其他未完成的,所有的goroutine都要全部完成才OK。
这种⽅式的优点:使⽤等待组的并发控制模型,尤其适⽤于好多个goroutine协同做⼀件事情的时候,因为每个goroutine做的都是这件事情的⼀部分,只有全部的goroutine都完成,这件事情才算完成;
这种⽅式的缺陷:在实际⽣产中,需要我们主动的通知某⼀个 goroutine 结束。
⽐如我们开启⼀个后台 goroutine ⼀直做事情,⽐如监控,现在不需要了,就需要通知这个监控 goroutine 结束,不然它会⼀直跑,就泄漏了。
通道 lect的⽅式
在等待组例⼦的最后抛出了⼀个问题,针对这种问题有2种办法:
1. 设置全局变量,在我们需要通知goroutine要停⽌的时候,我们为全局变量赋值,但是这样我们必须保证线程安全,不可避免的我们要
为全局变量加锁,在便利性及性能上稍显不⾜;
2. 使⽤chan+lect多路复⽤的⽅式,就会优雅许多;
package main
import (
"fmt"
"time"
)
func main() {
stop := make(chan bool)陈皮的禁忌
// 开启goroutine
go func() {
for {
lect {
ca <- stop:
fmt.Println("任务1 结束了。。。")
default:
fmt.Println(" 任务1 正在运⾏中。")
time.Sleep(time.Second * 2)
}
}
}()
// 运⾏10s后停⽌
time.Sleep(time.Second * 10)
fmt.Println("需要停⽌任务1。。。")
stop <- true
time.Sleep(time.Second * 3)
}
运⾏结果:
任务1 正在运⾏中。
任务1 正在运⾏中。
任务1 正在运⾏中。
任务1 正在运⾏中。
任务1 正在运⾏中。
需要停⽌任务1。。。
任务1 结束了。。。
上⾯例⼦中:我们定义⼀个 stop 的 chan,通知它结束后台 goroutine。
实现也⾮常简单,在后台 goroutine 中,使⽤ lect 判断 stop 是否可以接收到值,如果可以接收到,就表⽰可以退出停⽌了;如果没有接收到,就会执⾏ default ⾥的逻辑,继续运⾏,直到收到 stop 的通知。
发送了 stop<- true 结束的指令后,我这⾥使⽤ time.Sleep(3 * time.Second) 故意停顿 3 秒来检测我们结束任务goroutine 是否成功。
如果成功的话,不会再有 "任务1 正在运⾏中。" 的输出了;如果没有成功,监控 goroutine 就会继续打印 "任务1 正在运⾏中。" 输出。
channel配合lect⽅式的优点:⽐较优雅,
channel配合lect⽅式的劣势:如果有很多 goroutine 都需要控制结束怎么办?,如果这些 goroutine ⼜衍⽣了其它更多的goroutine 怎么办?
context的加⼊
context是GO1.7版本加⼊的⼀个标准库,它定义了Context类型,专门⽤来简化对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截⽌时间等相关操作,这些操作可能涉及多个 API 调⽤。
对服务器传⼊的请求应该创建上下⽂,⽽对服务器的传出调⽤应该接受上下⽂。它们之间的函数调⽤链必须传递上下⽂,或者可以使
⽤WithCancel、WithDeadline、WithTimeout或WithValue创建的派⽣上下⽂。当⼀个上下⽂被取消时,它派⽣的所有上下⽂也被取消。
当⼀个goroutine在衍⽣⼀个goroutine时,context可以跟踪到⼦goroutine,从⽽达到控制他们的⽬的;
使⽤context重写上⾯的lect例⼦
package main
import (
"fmt"
"time"
"context"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 开启goroutine,传⼊ctx
go func(ctx context.Context) {
for {
lect {
ca <- ctx.Done():
fmt.Println("任务1 结束了。。。")
return
default:
fmt.Println(" 任务1 正在运⾏中。")
time.Sleep(time.Second * 2)
}
}
}(ctx)
// 运⾏10s后停⽌
time.Sleep(time.Second * 10)喜鹊吃什么
fmt.Println("需要停⽌任务1。。。")
// 使⽤context的cancel函数停⽌goroutine
cancel()
// 为了检测监控过是否停⽌,如果没有监控输出,就表⽰停⽌了
time.Sleep(time.Second * 3)
}
重写⽐较简单,就是把原来的 chan stop 换成 Context,使⽤ Context 跟踪 goroutine,以便进⾏控制,⽐如结束等。
context.Background() 返回⼀个空的 Context,这个空的 Context ⼀般⽤于整个 Context 树的根节点。然后我们使⽤
context.WithCancel(parent) 函数,创建⼀个可取消的⼦ Context,然后当作参数传给 goroutine 使⽤,这样就可以使⽤这个⼦ Context 跟踪这个 goroutine。
在 goroutine 中,使⽤ lect 调⽤<-ctx.Done()判断是否要结束,如果接受到值的话,就可以返回结束 goroutine 了;如果接收不到,就会继续进⾏运⾏任务。
那么是如何发送结束指令的呢?这就是⽰例中的 cancel 函数啦,它是我们调⽤context.WithCancel(parent) 函数⽣成⼦ Context 的时候返回的,第⼆个返回值就是这个取消函数,它是 CancelFunc 类型的。我们调⽤它就可以发出取消指令,然后我们的监控 goroutine 就会收到信号,就会返回结束。
Context控制多个goroutine
package main
import (
"fmt"
"time"
"context"
)
// 使⽤context控制多个goroutine
func watch(ctx context.Context, name string) {
for {
lect {
ca <- ctx.Done():
fmt.Println(name, "退出,停⽌了。。。")
return
default:
fmt.Println(name, "运⾏中。。。")
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "【任务1】")
go watch(ctx, "【任务2】")
go watch(ctx, "【任务3】")
time.Sleep(time.Second * 10)
fmt.Println("通知任务停⽌。。。。")
cancel()
time.Sleep(time.Second * 5)
fmt.Println("真的停⽌了。。。")
}
上⾯例⼦中,启动了 3 个监控 goroutine 进⾏不断的运⾏任务,每⼀个都使⽤了 Context 进⾏跟踪,当我们使⽤ cancel 函数通知取消时,这3 个 goroutine 都会被结束。这就是 Context 的控制能⼒,它就像⼀个控制器⼀样,按下开关后,所有基于这个 Context 或者衍⽣的⼦Context 都会收到通知,这时就可以进⾏清理操作了,最终释放 goroutine,这就优雅的解决了 goroutine 启动后不可控的问题。context接⼝
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Context 的接⼝定义的⽐较简洁,这个接⼝共有 4 个⽅法;
1. Deadline :是获取设置的截⽌时间的意思,第⼀个返回值是截⽌时间,到了这个时间点,Context 会⾃动发起取消请求;第⼆个返回值
ok==fal时表⽰没有设置截⽌时间,如果需要取消的话,需要调⽤取消函数进⾏取消。
2. Done:该⽅法返回⼀个只读的 chan,类型为 struct{},我们在 goroutine 中,如果该⽅法返回的 chan 可以读取,则意味着parent context已
经发起了取消请求,我们通过 Done ⽅法收到这个信号后,就应该做清理操作,然后退出 goroutine,
释放资源。
3. Err⽅法返回取消的错误原因,因为什么 Context 被取消。
4. Value⽅法获取该 Context 上绑定的值,是⼀个键值对,所以要通过⼀个 Key 才可以获取对应的值,这个值⼀般是线程安全的。
四个⽅法中常⽤的就是 Done 了,如果 Context 取消的时候,我们就可以得到⼀个关闭的 chan,关闭的 chan 是可以读取的,所以只要可以读取的时候,就意味着收到 Context 取消的信号了,以下是这个⽅法的经典⽤法。
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
lect {
ca <-ctx.Done():
return ctx.Err()
ca out <- v:
}
}
}
Context 接⼝并不需要我们实现,Go 内置已经帮我们实现了 2 个,我们代码中最开始都是以这两个内置的作为最顶层的 partent context,衍⽣出更多的⼦ Context。
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
netbigfunc Background() Context {
return background
}
func TODO() Context {
return todo
}
1. Background()主要⽤于 main 函数、初始化以及测试代码中,作为 Context 这个树结构的最顶层的 Context,也就是根 Context。
2. TODO(),它⽬前还不知道具体的使⽤场景,如果我们不知道该使⽤什么 Context 的时候,可以使⽤这个。
它们两个本质上都是 emptyCtx 结构体类型,是⼀个不可取消,没有设置截⽌时间,没有携带任何值的 Context。
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
幼儿学习数字
}
这就是 emptyCtx 实现 Context 接⼝的⽅法,可以看到,这些⽅法什么都没做,返回的都是 nil 或者零值。
Context的继承衍⽣
有了如上的根 Context,那么是如何衍⽣更多的⼦ Context 的呢?这就要靠 context 包为我们提供的 With 系列的函数了。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
这四个 With 函数,接收的都有⼀个 partent 参数,就是⽗ Context,我们要基于这个⽗ Context 创建出⼦ Context 的意思,这种⽅式可以理解为⼦ Context 对⽗ Context 的继承,也可以理解为基于⽗ Context 的衍⽣。通过这些函数,就创建了⼀颗 Context 树,树的每个节点都可以有任意多个⼦节点,节点层级可以有任意多个。
1. WithCancel函数,传递⼀个⽗ Context 作为参数,返回⼦ Context,以及⼀个取消函数⽤来取消 Context。 WithDeadline 函数,和
WithCancel 差不多,它会多传递⼀个截⽌时间参数,意味着到了这个时间点,会⾃动取消 Context,当然我们也可以不等到这个时候,可以提前通过取消函数进⾏取消。
2. WithTimeout和 WithDeadline基本上⼀样,这个表⽰是超时⾃动取消,是多少时间后⾃动取消 Context 的意思。
3. WithValue函数和取消 Context ⽆关,它是为了⽣成⼀个绑定了⼀个键值对数据的 Context,即给context设置值,这个绑定的数据可以通
过 Context.Value ⽅法访问到.
上⾯3个函数都会返回⼀个取消函数CancelFunc,这是⼀个函数类型,它的定义⾮常简单type Cancel
Func func(),该函数可以取消⼀个Context,以及这个节点 Context下所有的所有的 Context,不管有多少层级。
Context传递元数据
package main
import (
"fmt"
"time"
"context"将军令歌曲
)
var key string = "name"
// 使⽤通过context向goroutinue传递值
func watch(ctx context.Context) {
for {
lect{
ca <- ctx.Done():
fmt.Println(ctx.Value(key), "退出,停⽌了。。。")
return
default:
fmt.Println(ctx.Value(key), "运⾏中...")
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
// 给ctx绑定键值,传递给goroutine
valuectx := context.WithValue(ctx, key, "【监控1】")
// 启动goroutine
go watch(valuectx)
time.Sleep(time.Second * 10)
fmt.Println("该结束了。。。")
// 运⾏结束函数
cancel()
time.Sleep(time.Second * 3)
fmt.Println("真的结束了。。")
}
注意点
1. context.WithValue ⽅法附加⼀对 K-V 的键值对,这⾥ Key 必须是等价性的,也就是具有可⽐性;Value 值要是线程安全的。
2. 在使⽤值的时候,可以通过 Value ⽅法读取 ctx.Value(key)。
3. 使⽤ WithValue 传值,⼀般是必须的值,不要什么值都传递。
Context最佳实战
1. 不要把 Context 放在结构体中,要以参数的⽅式传递
2. 以 Context 作为参数的函数⽅法,应该把 Context 作为第⼀个参数,放在第⼀位
3. 给⼀个函数⽅法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使⽤ context.TODO
4. Context 的 Value 相关⽅法应该传递必须的数据,不要什么数据都使⽤这个传递
5. Context 是线程安全的,可以放⼼的在多个 goroutine 中传递