前言
goroutine的存在,使得Go拥有比其他语言更好的并发执行特性。而在Go中,goroutine之间没有父与子的关系,也就没有所谓的子进程退出后的通知机制,多个goroutine都是平级的。多个goroutine的协同工作涉及到的通信、同步、通知和退出的四个方面的问题,Go也通过提供下面的方式来解决:通信:chan通道类型,这是goroutine之间通信的基础。同步:一个不带缓存的chan提供了一个天然的同步等待机制;另外sync.WaitGroup也可以为多个goroutine协同工作提供一种同步等待机制。
通知:一般地通过在输入端绑定两个chan,一个用于业务处理,一个用于异常通知,通过select收敛进行处理。退出:借助chan和select广播机制实现。
Context
context设计目的
context库的设计目的就是跟踪goroutine调用树,并在这些goroutine调用树中传递通知和元数据。
context工作机制
第一个创建context的goroutine作为根节点,根节点负责创建一个实现Context接口的具体对象,并将该对象作为参数传递给新的goroutine,下游的gorotine可以继续封装这个根节点,再传递到更下游的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{}
}
Deadline(): deadline返回ctx完成工作应该被取消的时间,当deadline没有被设置返回ok==false
Done(): 返回一个代表词context工作完成的管道(channel 这个也可以单开一篇),一般与select一起使用
Err(): 若done没有关闭则err返回空,若done closed(被cancel或者deadline超时),则err返回非空解释
Value(): 用于存储和上下文关联的key
Canceler接口
//canceler接口是一个扩展接口,规定了取消通知的Context具体类型需要实现的接口
type canceler interface {
//调用cancel方法通知后续创建的goroutine退出
cancel(removeFromParent bool,err error)
//此方法返回的chan需要后端的goroutine来监听并及时退出
Done() <-chan struct{}
}
Context实现的四个数据类型
emptyCtx
emptyCtx实现了Context接口,但不具备任何功能,所有方法都是空实现。其存在的目的就是作为Context对象树的根节点。
type emptyCtx int
cancelCtx
cancelCtx是一个实现了Context接口的具体类型,同时实现了canceler接口,其具备通知退出的能力。注意退出通知机制不但能通知自己,也能逐层通知其子节点。
type cancelCtx struct {
Context //父亲上下文
mu sync.Mutex // 保护并发
done atomic.Value //Value 提供一致类型值的原子加载和存储。Value 的零值从 Load 返回 nil。调用 Store 后,不得复制 Value。第一次使用后不得复制 Value。
children map[canceler]struct{} // 存储此上下文下的子上下文
err error // set to non-nil by the first cancel call
}
timerCtx
timerCtx在cancelCtx基础上,新增了一个deadline变量用于实现定时退出通知。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
valueCtx
valueCtx是一个实现了Context接口的具体类型,同时封装了一个k-v的存储变量,用于传递通知信息。
type valueCtx struct{
Context
key,val interface{}
}
创建不同功能的Context对象
- 创建一个带有退出通知的Context具体对象,内部创建一个cancelCtx的类型实例。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
2. 创建一个带有超时通知的Context对象,内部创建一个timerCtx的类型实例。
func WithDeadline(parent Context, deadline time.Time) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)
3. 创建一个能够传递数据的Context具体对象,内部创建一个valueCtx的类型实现。
func WithValue(parent Context, key, value interface{}) Context
Context代码示例
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctxA, cancel := context.WithCancel(context.Background())
go work(ctxA, "work_ctxA")
//基于ctxA创建ctxB
tm := time.Now().Add(3 * time.Second)
ctxB, _ := context.WithDeadline(ctxA, tm)
go work(ctxB, "work_ctxB")
//基于ctxB创建ctxC
ctxC := context.WithValue(ctxB, "key", " 程序员菜菜")
go workWithValue(ctxC, "work_ctxC")
time.Sleep(5 * time.Second)
cancel()
time.Sleep(5 * time.Second)
fmt.Println("主函数退出")
}
func work(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name + " 此进程被取消!")
return
default:
fmt.Println(name + " 此进程正在运行中。。。")
time.Sleep(1 * time.Second)
}
}
}
func workWithValue(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name + " 此进程被取消!")
return
default:
val := ctx.Value("key").(string)
fmt.Println(name + " 此进程正在运行中。它的值是" + val)
time.Sleep(1 * time.Second)
}
}
}
运行结果:
注意:使用context传递数据,其实只是context库的一个额外功能,它的主要目的还是解决goroutine的通知退出。在使用context传递信息时不能影响正常的业务流程,即传递的信息应只能是一些非必要的数据。例如:日志信息、调试信息以及不影响业务逻辑的可选数据。
在context中传递数据的坏处:
- 传递的都是interface类型的值,编译器无法进行严格的类型校验。
- 从interface到具体类型需要使用类型断言和接口查询,会有一定的性能损耗。
- 值在传递过程中有可能会被后续的服务覆盖,且不易被发现
- 传递信息不简明,较晦涩,不利于后续维护。