在上下文中,取消传播的流动

我进行了对取消传播的流程进行了调查。
作为一个简单的取消处理示例,我准备了以下的样本代码。请根据这个示例源代码来追踪流程。

示例代码

在主函数中启动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/
广告
将在 10 秒后关闭
bannerAds