Hi,你们好,我是明哥。html
在本身学习 Golang 的这段时间里,我写了详细的学习笔记放在个人我的微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,所以写的东西应该会比较适合刚接触的同窗,若是你也是刚学习 Go 语言,不防关注一下,一块儿学习,一块儿成长。git
个人在线博客:golang.iswbm.com 个人 Github:github.com/iswbm/GolangCodingTimegithub
在 Go 1.7 版本以前,context 仍是非编制的,它存在于 golang.org/x/net/context 包中。golang
后来,Golang 团队发现 context 还挺好用的,就把 context 收编了,在 Go 1.7 版本正式归入了标准库。编程
Context,也叫上下文,它的接口定义以下安全
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}复制代码
能够看到 Context 接口共有 4 个方法微信
Deadline
:返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动做。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,若是没有设置截止时间,就要手动调用 cancel 函数取消 Context。Done
:返回一个只读的通道(只有在被cancel后才会返回),类型为 struct{}
。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就能够作一些清理动做,退出goroutine。Err
:返回 context 被 cancel 的缘由。Value
:返回被绑定到 Context 的值,是一个键值对,因此要经过一个Key才能够获取对应的值,这个值通常是线程安全的。当一个协程(goroutine)开启后,咱们是没法强制关闭它的。并发
常见的关闭协程的缘由有以下几种:函数
第一种,属于正常关闭,不在今天讨论范围以内。学习
第二种,属于异常关闭,应当优化代码。
第三种,才是开发者能够手动控制协程的方法,代码示例以下:
func main() {
stop := make(chan bool)
go func() {
for {
select {
case <-stop:
fmt.Println("监控退出,中止了...")
return
default:
fmt.Println("goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("能够了,通知监控中止")
stop<- true
//为了检测监控过是否中止,若是没有监控输出,就表示中止了
time.Sleep(5 * time.Second)
}复制代码
例子中咱们定义一个stop
的chan,通知他结束后台goroutine。实现也很是简单,在后台goroutine中,使用select判断stop
是否能够接收到值,若是能够接收到,就表示能够退出中止了;若是没有接收到,就会执行default
里的监控逻辑,继续监控,只到收到stop
的通知。
以上是一个 goroutine 的场景,若是是多个 goroutine ,每一个goroutine 底下又开启了多个 goroutine 的场景呢?在 飞雪无情的博客 里关于为什么要使用 Context,他是这么说的
chan+select的方式,是比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,若是有不少goroutine都须要控制结束怎么办呢?若是这些goroutine又衍生了其余更多的goroutine怎么办呢?若是一层层的无穷尽的goroutine呢?这就很是复杂了,即便咱们定义不少chan也很难解决这个问题,由于goroutine的关系链就致使了这种场景很是复杂。
在这里我不是很赞同他说的话,由于我以为就算只使用一个通道也能达到控制(取消)多个 goroutine 的目的。下面就用例子来验证一下。
该例子的原理是:使用 close 关闭通道后,若是该通道是无缓冲的,则它会从原来的阻塞变成非阻塞,也就是可读的,只不过读到的会一直是零值,所以根据这个特性就能够判断 拥有该通道的 goroutine 是否要关闭。
package main
import (
"fmt"
"time"
)
func monitor(ch chan bool, number int) {
for {
select {
case v := <-ch:
// 仅当 ch 通道被 close,或者有数据发过来(不管是true仍是false)才会走到这个分支
fmt.Printf("监控器%v,接收到通道值为:%v,监控结束。\n", number,v)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
stopSingal := make(chan bool)
for i :=1 ; i <= 5; i++ {
go monitor(stopSingal, i)
}
time.Sleep( 1 * time.Second)
// 关闭全部 goroutine
close(stopSingal)
// 等待5s,若此时屏幕没有输出 <正在监控中> 就说明全部的goroutine都已经关闭
time.Sleep( 5 * time.Second)
fmt.Println("主程序退出!!")
}复制代码
输出以下
监控器4,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器3,正在监控中...
监控器5,正在监控中...
监控器2,接收到通道值为:false,监控结束。
监控器3,接收到通道值为:false,监控结束。
监控器5,接收到通道值为:false,监控结束。
监控器1,接收到通道值为:false,监控结束。
监控器4,接收到通道值为:false,监控结束。
主程序退出!!复制代码
上面的例子,说明当咱们定义一个无缓冲通道时,若是要对全部的 goroutine 进行关闭,可使用 close 关闭通道,而后在全部的 goroutine 里不断检查通道是否关闭(前提你得约定好,该通道你只会进行 close 而不会发送其余数据,不然发送一次数据就会关闭一个goroutine,这样会不符合我们的预期,因此最好你对这个通道再作一层封装作个限制)来决定是否结束 goroutine。
因此你看到这里,我作为初学者仍是没有找到使用 Context 的必然理由,我只能说 Context 是个很好用的东西,使用它方便了咱们在处理并发时候的一些问题,可是它并非不可或缺的。
换句话说,它解决的并非 能不能 的问题,而是解决 更好用 的问题。
若是不使用上面 close 通道的方式,还有没有其余更优雅的方法来实现呢?
有,那就是本文要讲的 Context
我使用 Context 对上面的例子进行了一番改造。
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
// 其实能够写成 case <- ctx.Done()
// 这里仅是为了让你看到 Done 返回的内容
case v :=<- ctx.Done():
fmt.Printf("监控器%v,接收到通道值为:%v,监控结束。\n", number,v)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i :=1 ; i <= 5; i++ {
go monitor(ctx, i)
}
time.Sleep( 1 * time.Second)
// 关闭全部 goroutine
cancel()
// 等待5s,若此时屏幕没有输出 <正在监控中> 就说明全部的goroutine都已经关闭
time.Sleep( 5 * time.Second)
fmt.Println("主程序退出!!")
}复制代码
这里面的关键代码,也就三行
第一行:以 context.Background() 为 parent context 定义一个可取消的 context
ctx, cancel := context.WithCancel(context.Background())复制代码
第二行:而后你能够在全部的goroutine 里利用 for + select 搭配来不断检查 ctx.Done() 是否可读,可读就说明该 context 已经取消,你能够清理 goroutine 并退出了。
case <- ctx.Done():复制代码
第三行:当你想到取消 context 的时候,只要调用一下 cancel 方法便可。这个 cancel 就是咱们在建立 ctx 的时候返回的第二个值。
cancel()复制代码
运行结果输出以下。能够发现咱们实现了和 close 通道同样的效果。
监控器3,正在监控中...
监控器4,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器2,接收到通道值为:{},监控结束。
监控器5,接收到通道值为:{},监控结束。
监控器4,接收到通道值为:{},监控结束。
监控器1,接收到通道值为:{},监控结束。
监控器3,接收到通道值为:{},监控结束。
主程序退出!!复制代码
建立 Context 必需要指定一个 父 Context,当咱们要建立第一个Context时该怎么办呢?
不用担忧,Go 已经帮咱们实现了2个,咱们代码中最开始都是以这两个内置的context做为最顶层的parent context,衍生出更多的子Context。
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}复制代码
一个是Background,主要用于main函数、初始化以及测试代码中,做为Context这个树结构的最顶层的Context,也就是根Context,它不能被取消。
一个是TODO,若是咱们不知道该使用什么Context的时候,可使用这个,可是实际应用中,暂时尚未使用过这个TODO。
他们两个本质上都是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
}
复制代码
上面在定义咱们本身的 Context 时,咱们使用的是 WithCancel
这个方法。
除它以外,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复制代码
这四个函数有一个共同的特色,就是第一个参数,都是接收一个 父context。
经过一次继承,就多实现了一个功能,好比使用 WithCancel 函数传入 根context ,就建立出了一个子 context,该子context 相比 父context,就多了一个 cancel context 的功能。
若是此时,咱们再以上面的子context(context01)作为父context,并将它作为第一个参数传入WithDeadline函数,得到的子子context(context02),相比子context(context01)而言,又多出了一个超过 deadline 时间后,自动 cancel context 的功能。
接下来我会举例介绍一下这几种 context,其中 WithCancel 在上面已经讲过了,下面就再也不举例了
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case <- ctx.Done():
fmt.Printf("监控器%v,监控结束。\n", number)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx01, cancel := context.WithCancel(context.Background())
ctx02, cancel := context.WithDeadline(ctx01, time.Now().Add(1 * time.Second))
defer cancel()
for i :=1 ; i <= 5; i++ {
go monitor(ctx02, i)
}
time.Sleep(5 * time.Second)
if ctx02.Err() != nil {
fmt.Println("监控器取消的缘由: ", ctx02.Err())
}
fmt.Println("主程序退出!!")
}复制代码
输出以下
监控器5,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器3,正在监控中...
监控器4,正在监控中...
监控器3,监控结束。
监控器4,监控结束。
监控器2,监控结束。
监控器1,监控结束。
监控器5,监控结束。
监控器取消的缘由: context deadline exceeded
主程序退出!!复制代码
WithTimeout 和 WithDeadline 使用方法及功能基本一致,都是表示超过必定的时间会自动 cancel context。
惟一不一样的地方,咱们能够从函数的定义看出
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)复制代码
WithDeadline 传入的第二个参数是 time.Time 类型,它是一个绝对的时间,意思是在什么时间点超时取消。
而 WithTimeout 传入的第二个参数是 time.Duration 类型,它是一个相对的时间,意思是多长时间后超时取消。
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case <- ctx.Done():
fmt.Printf("监控器%v,监控结束。\n", number)
return
default:
fmt.Printf("监控器%v,正在监控中...\n", number)
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx01, cancel := context.WithCancel(context.Background())
// 相比例子1,仅有这一行改动
ctx02, cancel := context.WithTimeout(ctx01, 1* time.Second)
defer cancel()
for i :=1 ; i <= 5; i++ {
go monitor(ctx02, i)
}
time.Sleep(5 * time.Second)
if ctx02.Err() != nil {
fmt.Println("监控器取消的缘由: ", ctx02.Err())
}
fmt.Println("主程序退出!!")
}复制代码
输出的结果和上面同样
监控器1,正在监控中...
监控器5,正在监控中...
监控器3,正在监控中...
监控器2,正在监控中...
监控器4,正在监控中...
监控器4,监控结束。
监控器2,监控结束。
监控器5,监控结束。
监控器1,监控结束。
监控器3,监控结束。
监控器取消的缘由: context deadline exceeded
主程序退出!!复制代码
经过Context咱们也能够传递一些必须的元数据,这些数据会附加在Context上以供使用。
元数据以 Key-Value 的方式传入,Key 必须有可比性,Value 必须是线程安全的。
仍是用上面的例子,以 ctx02 为父 context,再建立一个能携带 value 的ctx03,因为他的父context 是 ctx02,因此 ctx03 也具有超时自动取消的功能。
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case <- ctx.Done():
fmt.Printf("监控器%v,监控结束。\n", number)
return
default:
// 获取 item 的值
value := ctx.Value("item")
fmt.Printf("监控器%v,正在监控 %v \n", number, value)
time.Sleep(2 * time.Second)
}
}
}
func main() {
ctx01, cancel := context.WithCancel(context.Background())
ctx02, cancel := context.WithTimeout(ctx01, 1* time.Second)
ctx03 := context.WithValue(ctx02, "item", "CPU")
defer cancel()
for i :=1 ; i <= 5; i++ {
go monitor(ctx03, i)
}
time.Sleep(5 * time.Second)
if ctx02.Err() != nil {
fmt.Println("监控器取消的缘由: ", ctx02.Err())
}
fmt.Println("主程序退出!!")
}复制代码
输出以下
监控器4,正在监控 CPU
监控器5,正在监控 CPU
监控器1,正在监控 CPU
监控器3,正在监控 CPU
监控器2,正在监控 CPU
监控器2,监控结束。
监控器5,监控结束。
监控器3,监控结束。
监控器1,监控结束。
监控器4,监控结束。
监控器取消的缘由: context deadline exceeded
主程序退出!!复制代码