序言:
关于 go的并发编程
正文
前言需知, channel 的使用 https://blog.csdn.net/dongjijiaoxiangqu/article/details/109642733
1.context 的使用场景
- context 的含义,context 意为上下文, go中的context 的主要使用场景为在多个goroutine 中进行 通信,避免浪费无用的计算资源。
打个比方:
比如说一个 网络请求中, 系统起了一个 go routine A来服务此请求, 同时 A routine 中,同时需要进行多个任务,那么同时起了 B,C,D 三个 go routine 来进行处理,
而网络请求可能有一个超时处理时间,又或者是客户端掐断了网络请求,这时候 A 得到了请求中断的通知,需要将 这个信息带给 B,C,D 三个 go routine, 让它们中断处理,节省资源,此时就可以通过context 进行信息传递。
2. context 的类型
- context 首先是一个接口, context 体系都是以这个 接口对外暴露方法的
type Context interface {
// 是否设置了超时时间
Deadline() (deadline time.Time, ok bool)
// 是否是已经做完
Done() <-chan struct{}
// 是否有错误发生
Err() error
// 是否带有value
Value(key interface{}) interface{}
} |
context 的类型
首先是 空类型的context,使用场景不大,但是其他context 构建的时候需要父context
空的context : backgound, todo
其次是,有实际意义的context,例如 带有cancel 的context, 带有超时的context
] // 返回一个context, 一个cancelFunc ,可以通过cancelFunc 通知取消
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
}
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
func WithValue(parent Context, key, val interface{}) Context {
} |
四种类型的context:
- WithCancel 带取消 func 的context
- WithDeadline 带deadline 超时的context
- WithTimeout 同样也是带超时的context
- WithValue 能在不同context 进行值传递的context
3. 说完类型,下面我们说一说这几种context的使用方式
- 回到上面的使用场景,我们要想想这个组件为解决何事而生,没有它的时候,我们是怎么解决这个问题的,如果有了这个组件是不是解决此类问题,更加便捷了。
回看 context的使用场景,我们知道了context的使用场景。
这种的话,我们很容易想到不同 routine 之间的通信方式 channel, 通过channel的相互信息传递,我们可以做到 不同routine之间的信息传递。其实context 就是 channel的一类封装,使其使用更加便利。
思维扩散:
假如在不同语言中,我们的解决方案又是什么呢?
java:
在java中,我们在一个线程中,另起一个线程进行计算,当我们想取消 另一个线程的计算,我们会采用 将计算线程提交到线程池,返回 Future 的方式来控制,通过 Futrue 我们可以取消,超时控制的方式控制另一个线程的运行。我们以此思考再思考下本质,
但在java中,我们调用 future的 cancel 并不能立马暂停线程的执行,只是把当前Thread.currentThread().isInterrupted() 置为true,暂停当前线程代码,还得业务方进行判断。
经过实验是这样,实验代码
/**
* @author: liudeyu
* @date: 2020/11/11
*/
public class InterruptThreadTest {
ExecutorService executor = Executors.newFixedThreadPool(4);
public static class AThread implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int index = -1;
int reverCount = 1000000;
// isinterrupted 判断才能生效
while (index < 1000/* && !Thread.currentThread().isInterrupted()*/) {
index++;
System.out.println(index);
// Thread.sleep(1000);
Node tmpList = NodeUtils.createRandomeIntList(reverCount, true);
Node tmpList2 = NodeUtils.createRandomeIntList(reverCount, true);
MergeKSortLinkedList mergeKSortLinkedList = new MergeKSortLinkedList();
mergeKSortLinkedList.mergeKLinkedList(new Node[]{tmpList, tmpList2});
}
System.out.println("being interupt");
return 1;
}
}
public static void main(String[] args) throws InterruptedException {
InterruptThreadTest interruptThreadTest = new InterruptThreadTest();
Future<Integer> future = interruptThreadTest.executor.submit(new AThread());
Thread.sleep(5000);
future.cancel(true);
System.out.println("cancel done");
// interruptThreadTest.executor.shutdown();
// try {
// Integer tmpRes = future.get(300, TimeUnit.MICROSECONDS);
//
// System.out.println(tmpRes);
// } catch (InterruptedException e) {
// e.printStackTrace();
// } catch (ExecutionException e) {
// e.printStackTrace();
// } catch (TimeoutException e) {
// e.printStackTrace();
// }finally {
// future.cancel(true);
// }
}
}
|
所以其暂停其他线程的本质是通过更改变量,检测共享变量的值,来进行线程是否需要退出。
在 nodejs ,由于是单线程模型,但是单线程的任务调度,可能还是需要共享变量来进行判断。
而go中利用的是 CSP 管道通知的方式,来进行多个routine之间的通信。
3. context 的具体使用
func backgroundContextUse() {
tmpCtx := context.Background()
deadLineCtx,cancelFun:=context.WithDeadline(tmpCtx,time.Now().Add(time.Second*3))
go func(a context.Context) {
fmt.Printf("I am doing some job\n")
select {
case <-a.Done():
fmt.Printf("task done ,exit\n")
}
}(deadLineCtx)
time.Sleep(14 * time.Second)
cancelFun()
fmt.Printf("cancel fun")
}
func main() {
//cancelCtxUse()
//testDefer()
backgroundContextUse()
time.Sleep(time.Minute)
} |
输出情况为:
I am doing some job
task done ,exit
cancel fun
|
解释:
如上述代码可以看出,这里声明了一个具有deadline 的context,在指定到期时间后就会通知相应goroutine, 就是其中的
select {
case ← a.Done():
省略
}
同时声明deadline context 返回 cancel 函数,可以调用 cancel 函数, 相应协程也会通知 goroutine,也就是 context.Done()也会通知到各个协程
cancelContext() 和 timeoutContext() 也是比较简单的使用,看其函数声明即可知
![](https://i-blog.csdnimg.cn/blog_migrate/c83f348ce5f5eba5e507cdd58d0bb8ff.png)
valueContext 是可以传递 key value的context 具体中使用场景较少
4. context 的实现原理
我们知道 context 是 channel的封装,我们来看下,其具体实现
首先我们看下 cancelContext 的具体实现,因为其他的 deadlineContext 和 timeOutContext 也是八九不离十
从其构造函数开始说起
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
|
解释:
首先要求一个parentContext, 不能为nil, 然后根据此结构,我们可以猜测 cancelContext 也可以声明cancelContext ,它是一个树形结构,这个的含义就是 父节点 调用了cancel() 以后,子节点也能自动取消其他goroutine的计算
这里简单给个例子,我们先来看一段示例代码
func mutipleCancelContextUsage() {
paCancelCtx, paCanFun := context.WithCancel(context.Background())
subCancelCtx, _ := context.WithCancel(paCancelCtx)
subCancelCtx2, canFun2 := context.WithCancel(paCancelCtx)
grandSubCtx3, _ := context.WithCancel(subCancelCtx2)
var testSubContextUsageFun = func(tmp context.Context, tag string) {
fmt.Printf(tag+" doing job %v\n", tmp)
select {
case <-tmp.Done():
//fmt.Printf("job done signal trigger\n")
fmt.Println(tag, " ", tmp.Err())
}
}
go testSubContextUsageFun(subCancelCtx, "subCancelCtx")
go testSubContextUsageFun(subCancelCtx2, "subCancelCtx2")
go testSubContextUsageFun(grandSubCtx3, "grandSubCtx3")
time.Sleep(5 * time.Second)
canFun2()
time.Sleep(15 * time.Second)
fmt.Println("after 15 sec")
paCanFun()
}
func main() {
//cancelCtxUse()
//testDefer()
//backgroundContextUse()
mutipleCancelContextUsage()
time.Sleep(time.Minute)
} |
上面这段代码输出情况为:
subCancelCtx doing job context.Background.WithCancel.WithCancel
grandSubCtx3 doing job context.Background.WithCancel.WithCancel.WithCancel
subCancelCtx2 doing job context.Background.WithCancel.WithCancel
subCancelCtx2 context canceled
grandSubCtx3 context canceled
after 15 sec
subCancelCtx context canceled
|
解释:
首先我们可以看到 上面的代码演示了不同 cancelContext 经过不同parent衍生出来的,每个parent能cancel 控制其子孙类
也就是
- subCancelCtx - grandSubCtx3
paCancelCtx
- subCancelCtx2
可以看到 输出 context 的字符串时候,可以看到
subCancelCtx : context.Background.WithCancel.WithCancel 是 background → cancelContext → cancelContext 连在一起
到 grandSubCtx3 就是 三个连在一起了 context.Background.WithCancel.WithCancel.WithCancel
回到主题,这样的树形结构的便利就在于,我们想要结束其中一个分支及其衍生分支的 产生的goroutine的运行时候,而不影响其他分支的运行时候 ,可以直接调用父节点的 cancel 方法,而相比原始channel ,我们要做到这一点就麻烦很多,最后产生的可能也是 这个context 结构
4.2 context 的源码解析
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
} |
先看下 newCancelCtx 是怎么实现的
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
} |
可以看到 cancelCtx 实现了 Context 接口,不难理解,因为CancelCtx 也是 Context 的一个实现类
这段的主体无疑就是 cancelCtx 这个结构体,我们简单看下,其实现的接口
![](https://i-blog.csdnimg.cn/blog_migrate/341618a788c13044b88b8c4ce0e6d8c3.png)
其中知道的比较明显的在此不表,我们知道其继承了 Context 接口了,那么在此基础上,我们能进行cancel() 调用,其重点应该另有接口,这时候我们的目光就被这个canceler 吸引了,我们简单看下其实现。
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
} |
好,这下引出了 重点, cancel 是怎么实现的,以及 Done() <-chan struct{} 的又一个接口,与Context 里面的一个 函数名相同。
根据接口重点,我们依次看下 cancel 是怎么实现 Context 和 canceler 接口的
func (c *cancelCtx) Value(key interface{}) interface{} {
if key == &cancelCtxKey {
return c
}
return c.Context.Value(key)
}
// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int |
Value 函数, 主要是拿来取值的,如果key 是固定的cancelCtxKey ,就直接返回自身
还有比较简单的接口
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
} |
返回相关的字段信息,deadline 接口默认继承 parent 的,不实现
接下来就是 canceler 里面的
// closedchan is a reusable closed channel. var closedchan = make(chan struct{}) // cancel closes c.done, cancels each of c's children, and, if // removeFromParent is true, removes c from its parent's children. func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err // 如果 done 为 nil ,还没进行赋值 if c.done == nil { c.done = closedchan } else { // 通知 channel done,通知所有持有的goroutine, close()比较特别,记得前面章节提的吗,这里的close能进行广播通知,所有持有的goroutine close(c.done) } for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. // 递归取消child child.cancel(false, err) } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) } } // removeChild removes a context from its parent. func removeChild(parent Context, child canceler) { p, ok := parentCancelCtx(parent) if !ok { return } p.mu.Lock() if p.children != nil { delete(p.children, child) } p.mu.Unlock() } |
我们可以看到函数注释,说得比较明确了,这个函数的意义就是取消父节点和其依存的子节点的gorutine,通知各个goroutine 的 context.Done() channel 通知, 然后加上 是否把自己从其父节点删除(ps ,在代码里面加了注释解释)
我觉得这个函数还是比较好懂,清晰的。
回到前面,我们还记得这个函数的调用处吗,这里简单回顾下
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled") |
看到了吗,返回给用户的cancel 函数里面 调用了这个 真正的cancel 函数。
看完了 cancel 方法,回到构造函数那里,我们需要看到,context 如何和它的子context 联系起来呢,这里就是这个 propagateCancel实现了,我们看其实现
把解释写进代码注释里面
func propagateCancel(parent Context, child canceler) { done := parent.Done() // 为nil 的话,说明 父 context 是 background context 或者其他 empty ctx类型 if done == nil { return // parent is never canceled } select { case <-done: // parent is already canceled // 这里意义比较明显,就是 parent done 的时候,把 son 节点的 也cancel 掉, 记得这里 cancel context 关的时候,会使用close(cancelCtx.Done) ,这里的话,所有的子routine 都会被通知到 child.cancel(false, parent.Err()) return default: } if p, ok := parentCancelCtx(parent); ok { // 如果parent ctx 为 cancelCtx p.mu.Lock() if p.err != nil { // parent has already been canceled, 这里容易理解,parent ctx 都被关掉了,err !=nil 表示为 parent ctx 被cancel 掉了,cancel 掉当前 child child.cancel(false, p.err) } else { // 判断是否初始化过 children map if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { // 当前如果不是cancelCtx 情况,如果检测到 parent done,也cancel child ,感觉和上面逻辑有重复之处,可能是自定义的context 时候,也要注意监测 atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } } // 这个函数返回 传入的 context 是否为 cancelCtx, 注意的是 timeoutCtx 也是继承了 cancelCtx // parentCancelCtx returns the underlying *cancelCtx for parent. // It does this by looking up parent.Value(&cancelCtxKey) to find // the innermost enclosing *cancelCtx and then checking whether // parent.Done() matches that *cancelCtx. (If not, the *cancelCtx // has been wrapped in a custom implementation providing a // different done channel, in which case we should not bypass it.) func parentCancelCtx(parent Context) (*cancelCtx, bool) { done := parent.Done() if done == closedchan || done == nil { return nil, false } p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) if !ok { return nil, false } p.mu.Lock() ok = p.done == done p.mu.Unlock() if !ok { return nil, false } return p, true } |
整体来说cancelCtx 就已经讲完,然后 timeCtx 和 cancelCtx 类似,但多了一个 time.After 的超时监控,看下timeCtx ,你就可以理解,为啥cancelCtx是关键, 继承了 cancelCtx,所以cancelCtx 的功能,timeCtx都有,还有了一个超时功能
// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
} |
结尾
go的context 就介绍到这,以较为详细篇幅讲述了 这个context 这个组件,树形结构的设计, 依靠 树形结构,和 close(channel)广播通知,实现了 父子节点的 gorutine关系的通知,同时达到了,序言说的目的,节省计算资源的目的,当最上层的goroutine都不在计算了,优雅通知
衍生出来的goroutine可以进行优雅停机,一般情况下可以在子routine中检测到停止信号,进行抛异常或退出操作。