在上下文中,取消传播的流动
我进行了对取消传播的流程进行了调查。
作为一个简单的取消处理示例,我准备了以下的样本代码。请根据这个示例源代码来追踪流程。
示例代码
在主函数中启动goroutine,并调用sleepFunc,在其中再次启动goroutine,并执行time.Sleep。
package main
import (
"context"
"fmt"
"time"
)
func sleepFunc(ctx context.Context, duration time.Duration) string {
doneCh := make(chan struct{})
go func() {
time.Sleep(duration * time.Second)
doneCh <- struct{}{}
}()
select {
case <-doneCh:
return "done"
case <-ctx.Done():
//ここでキャンセル処理を行う
return "cancel"
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
resultCh := make(chan string)
go func() {
resultCh <- sleepFunc(ctx, 10)
}()
cancel()
fmt.Println(<-resultCh)
}
在`sleepFunc()`函数内部启动的goroutine中执行`time.Sleep()`,但由于在`time.Sleep()`完成之前,`cancel()`函数在`main`函数中被调用,所以在标准输出中显示”cancel”。
您可以在Go Playground上确认此操作。
結果
cancelCtx構造体的done chan struct{}型,在 <- ctx.Done() 处进行等待,通过关闭(done)来传播取消信号。
获取上下文
ctx, cancel := context.WithCancel(context.Background())
在main函数中,首先使用context.Background()作为参数调用context.WithCancel()以获取context。
后台上下文()
context.Background() 函数返回一个空的 background context。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L202-L208
background是使用new()函数初始化的emptyCtx。
emptyCtx是一个满足Context接口的int类型变量,但它实现的方法中,除了String()方法以外,其他方法都没有实际处理逻辑,只是简单地返回。
取消带上下文。
context.WithCancel()将接收到的context作为父上下文,并返回一个新的context。
在示例代码中,将context.Background()作为参数传递,因此可以认为传递了一个已初始化的emptyCtx对象。
您可以在以下链接找到更多信息:
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L224-L234
我們將深入了解其中的內容。
newCancelCtx() 是一个函数,它初始化并返回 cancelCtx 结构体。
cancelCtx 结构体内嵌了 Context 接口,并定义了其他几个字段。
详细定义可参考以下链接:
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L236-L239
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L314-L323
在下一行中,使用parent和&c作为参数来执行propagateCancel()函数。
可以想象通过父上下文和派生的上下文(地址)来进行某些处理。
在propagateCancel()函数中,首先会检查parent.Done()是否为nil,如果是nil的话,则直接返回。
在示例代码中,parent是一个初始化为emptyCtx的对象,因此会调用emptyCtx中实现的Done()方法。该方法会返回nil,所以在这里直接返回。
在最后,使用newCancenCtx()函数获取新的上下文地址,并返回取消函数。这些被存储在名为ctx和cancel的变量中,这些变量在main函数中被存储。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L233
取消操作
在示例代码中,获取上下文后,启动协程并执行sleepFunc()。同时,将上下文作为参数传递。
sleepFunc()在内部启动了一个goroutine并执行了time.Sleep()。
在goroutine的外部,通过select等待channel的接收。如果time.Sleep()正常完成,则接收到doneCh;如果执行了取消操作,则接收到ctx.Done()。
由于在main函数中启动了goroutine并立即执行了cancel(),因此实际上会从ctx.Done()接收到信号,并在标准输出中显示字符串”cancel”。
取消()
在查看取消信号传播流程之前,我首先想要看一下ctx.Done()的内容。
使用cancelCtx中包含的synx.Mutex获取锁定。
根据注释中定义的位置,可以推断出它用于保护其他字段。
如果c.done为nil,则将初始化后的channel赋值给c.done。然后将c.done赋值给d并返回。
另外,由于Channel是引用类型,所以我们将其赋值给了d,这样d和c.done将引用相同的数据结构。
https://golang.org/doc/effective_go.html#channels
像地图一样,通道也是通过make分配的,并且生成的值充当对底层数据结构的引用。
取消()
这是取消传播的流程。
cancel 变量是从 context.WithCancel() 的第二个返回值中获取的。
context.WithCancel() 的第二个返回值是一个 func() { c.cancel(true, Canceled) } 函数,因此要追踪取消流程,可以查看 cancelCtx 的 cancel() 方法的内部实现。
如果err==nil,则会引发panic错误。err是作为第二个参数传递的Canceled错误。这个错误是通过errors.New()定义的并且被赋值,因此不是nil,接下来会执行下一步操作。
GitHub链接:
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L348-L350
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L154-L155
请注意,这只是一个简单的翻译,某些上下文可能会有所变化。
使用cancelCtx的synx.Mutex字段来获取锁定。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L351
如果c.err != nil,则被视为已取消,并在释放锁之后返回。
通过将err赋值给c.err,因此当cancel()被执行时,c.err将不再是nil,并且在下一次调用时,c.err != nil将成立。
(来源:https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L352-L356)
如果`c.done == nil`,那么`closedchan`被分配给`c.done`,否则`c.done`被关闭。
如果事先执行了`ctx.Done()`,那么由于`c.done`被设置为初始化的channel,该channel会被关闭。
也就是说,如果使用了`case <-ctx.Done():`这样的语句,那么在`c.done`被关闭时会收到信号,通过这种方式取消操作会被传播。
(GitHub链接的代码)
在这里定义了closedchan。 它是一个空结构的通道。
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L307-L308
正在执行c.children中每个元素的cancel()方法。
如果第一个参数removeFromParent为true,则执行removeChild()函数。
请参考以下链接:
https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L369-L371
如果存在上下文,则从父上下文的子上下文映射中自我删除。
请参考此链接:https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go#L287-L298
总结
我打算看一下使用context.WithTimeout()和context.WithValue()的情况,以及取消多个goroutine的流程。
文献引用
-
- https://github.com/golang/go/blob/release-branch.go1.10/src/context/context.go
-
- https://golang.org/pkg/context/
-
- https://blog.golang.org/context
- https://deeeet.com/writing/2016/07/22/context/