WithCancel能够将一个Context包装为cancelCtx,并提供一个勾销函数,调用这个勾销函数,能够Cancel对应的Context

Go语言context包-cancelCtx


疑难


context.WithCancel()勾销机制的了解勾销机制的了解")

父母5s钟后出门,倒计时,父母在时要学习,父母一走就能够玩

package mainimport (    "context"    "fmt"    "time")func dosomething(ctx context.Context) {    for {        select {        case <-ctx.Done():            fmt.Println("playing")            return        default:            fmt.Println("I am working!")            time.Sleep(time.Second)        }    }}func main() {    ctx, cancelFunc := context.WithCancel(context.Background())    go func() {        time.Sleep(5 * time.Second)        cancelFunc()    }()    dosomething(ctx)}


为什么调用cancelFunc就能从ctx.Done()里获得返回值? 进而勾销对应的Context?


温习一下channel的一个个性


从一个曾经敞开的channel里能够始终获取对应的零值


WithCancel代码剖析


pkg.go.dev/context#WithCancel:

// WithCancel returns a copy of parent with a new Done channel. The returned// context's Done channel is closed when the returned cancel function is called// or when the parent context's Done channel is closed, whichever happens first.//// Canceling this context releases resources associated with it, so code should// call cancel as soon as the operations running in this Context complete.//WithCancel 返回具备新 Done 通道的 parent 正本。 返回的上下文的实现通道在调用返回的勾销函数或父上下文的实现通道敞开时敞开,以先产生者为准。//勾销此上下文会开释与其关联的资源,因而代码应在此上下文中运行的操作实现后立刻调用勾销。func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {    if parent == nil {        panic("cannot create context from nil parent")    }    c := newCancelCtx(parent)   // 将parent作为父节点context 生成一个新的子节点    //取得“父Ctx门路”中可被勾销的Ctx    //将child canceler退出该父Ctx的map中    propagateCancel(parent, &c)    return &c, func() { c.cancel(true, Canceled) }}

WithCancel最初返回 子上下文和一个cancelFunc函数,而cancelFunc函数里调用了cancelCtx这个构造体的办法cancel

(代码基于go 1.16; 1.17有所改变)

// A cancelCtx can be canceled. When canceled, it also cancels any children// that implement canceler.type cancelCtx struct {    Context    mu       sync.Mutex            // protects following fields    done     chan struct{}         // created lazily, closed by first cancel call done是一个channel,用来 传递敞开信号    children map[canceler]struct{} // set to nil by the first cancel call  children是一个map,存储了以后context节点下的子节点    err      error                 // set to non-nil by the first cancel call  err用于存储错误信息 示意工作完结的起因}

在cancelCtx这个构造体中,字段done是一个传递空构造体类型的channel,用来在上下文勾销时敞开这个通道,err就是在上下文被勾销时通知用户这个上下文勾销了,能够用ctx.Err()来获取信息

canceler是一个实现接口,用于Ctx的终止。实现该接口的Context有cancelCtx和timerCtx,而emptyCtx和valueCtx没有实现该接口。

// 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{}}
// closedchan is a reusable closed channel.var closedchan = make(chan struct{})func init() {    close(closedchan)}// cancel closes c.done, cancels each of c's children, and, if// removeFromParent is true, removes c from its parent's children./*** 1、cancel(...)以后Ctx的子节点* 2、从父节点中移除该Ctx**/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    //  设置一个敞开的channel或者将done channel敞开,用以发送敞开信号    if c.done == nil {        c.done = closedchan    } else {        close(c.done) // 留神这一步    }     // 将子节点context顺次勾销    for child := range c.children {        // NOTE: acquiring the child's lock while holding parent's lock.        child.cancel(false, err)    }    c.children = nil    c.mu.Unlock()    if removeFromParent {         // 将以后context节点从父节点上移除        removeChild(c.Context, c)    }}

对于cancel函数,其勾销了基于该上下文的所有子上下文以及把本身从父上下文中勾销

对于更多removeFromParent代码剖析,和其余Context的应用,强烈建议浏览 深刻了解Golang之Context(可用于实现超时机制)


    // Done is provided for use in select statements:    //    //  // Stream generates values with DoSomething and sends them to out    //  // until DoSomething returns an error or ctx.Done is closed.    //  func Stream(ctx context.Context, out chan<- Value) error {    //      for {    //          v, err := DoSomething(ctx)    //          if err != nil {    //              return err    //          }    //          select {    //          case <-ctx.Done():    //              return ctx.Err()    //          case out <- v:    //          }    //      }    //  }    //    // See https://blog.golang.org/pipelines for more examples of how to use    // a Done channel for cancellation.    Done() <-chan struct{}    // If Done is not yet closed, Err returns nil.    // If Done is closed, Err returns a non-nil error explaining why:    // Canceled if the context was canceled    // or DeadlineExceeded if the context's deadline passed.    // After Err returns a non-nil error, successive calls to Err return the same error.    Err() error

当调用cancelFunc()时,会有一步close(d)的操作,

ctx.Done 获取一个只读的 channel,类型为构造体。可用于监听以后 channel 是否曾经被敞开。

Done()用来监听cancel操作(对于cancelCtx)或超时操作(对于timerCtx),当执行勾销操作或超时时,c.done会被close,这样就能从一个曾经敞开的channel里始终获取对应的零值<-ctx.Done便不会再阻塞

(代码基于go 1.16; 1.17有所改变)

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}


总结一下:应用context.WithCancel时,除了返回一个新的context.Context(上下文),还会返回一个cancelFunc。 在须要勾销该context.Context时,就调用这个cancelFunc,之后以后上下文及其子上下文都会被勾销,所有的 Goroutine 都会同步收到这一勾销信号

至于cancelFunc是如何做到的?

在用户代码,for循环里select一直尝试从 <-ctx.Done()里读取出内容,但此时并没有任何给 c.done这个channel写入数据的操作,(相似c.done <- struct{}{}),故而在for循环里每次select时,这个case都不满足条件,始终阻塞着。每次都执行default代码段

而在执行cancelFunc时, 在func (c *cancelCtx) cancel(removeFromParent bool, err error)外面,会有一个close(c.done)的操作。而从一个曾经敞开的channel里能够始终获取对应的零值,即 select能够命中,进入case res := <-ctx.Done():代码段


可用如下代码验证:

package mainimport (    "context"    "fmt"    "time")func dosomething(ctx context.Context) {    var cuiChan = make(chan struct{})    go func() {        cuiChan <- struct{}{}    }()    //close(cuiChan)    for {        select {        case res := <-ctx.Done():            fmt.Println("res:", res)            return        case res2 := <-cuiChan:            fmt.Println("res2:", res2)        default:            fmt.Println("I am working!")            time.Sleep(time.Second)        }    }}func main() {    test()    ctx, cancelFunc := context.WithCancel(context.Background())    go func() {        time.Sleep(5 * time.Second)        cancelFunc()    }()    dosomething(ctx)}func test() {    var testChan = make(chan struct{})    if testChan == nil {        fmt.Println("make(chan struct{})后为nil")    } else {        fmt.Println("make(chan struct{})后不为nil!!!")    }}

输入:

make(chan struct{})后不为nil!!!I am working!res2: {}I am working!I am working!I am working!I am working!res: {}

而如果 不向没有缓存的cuiChan写入数据,间接close,即

package mainimport (    "context"    "fmt"    "time")func dosomething(ctx context.Context) {    var cuiChan = make(chan struct{})    //go func() {    //    cuiChan <- struct{}{}    //}()    close(cuiChan)    for {        select {        case res := <-ctx.Done():            fmt.Println("res:", res)            return        case res2 := <-cuiChan:            fmt.Println("res2:", res2)        default:            fmt.Println("I am working!")            time.Sleep(time.Second)        }    }}func main() {    test()    ctx, cancelFunc := context.WithCancel(context.Background())    go func() {        time.Sleep(5 * time.Second)        cancelFunc()    }()    dosomething(ctx)}func test() {    var testChan = make(chan struct{})    if testChan == nil {        fmt.Println("make(chan struct{})后为nil")    } else {        fmt.Println("make(chan struct{})后不为nil!!!")    }}

则会始终命中case 2

res2: {}res2: {}res2: {}res2: {}res2: {}res2: {}res2: {}...//始终打印上来


更多参考:

深刻了解Golang之Context(可用于实现超时机制)

答复我,进行 Goroutine 有几种办法?

golang context的done和cancel的了解 for循环channel实现context.Done()阻塞输入阻塞输入")




更多对于channel阻塞与close的代码


package mainimport (    "fmt"    "time")func main() {    ch := make(chan string, 0)    go func() {        for {            fmt.Println("----开始----")            v, ok := <-ch            fmt.Println("v,ok", v, ok)            if !ok {                fmt.Println("完结")                return            }            //fmt.Println(v)        }    }()    fmt.Println("<-ch始终没有货色写进去,会始终阻塞着,直到3秒钟后")    fmt.Println()    fmt.Println()    time.Sleep(3 * time.Second)    ch <- "向ch这个channel写入第一条数据..."    ch <- "向ch这个channel写入第二条数据!!!"    close(ch) // 当channel被close后, v,ok 中的ok就会变为false    time.Sleep(10 * time.Second)}

输入为:

----开始----<-ch始终没有货色写进去,会始终阻塞着,直到3秒钟后v,ok 向ch这个channel写入第一条数据... true----开始----v,ok 向ch这个channel写入第二条数据!!! true----开始----v,ok  false完结



package mainimport (    "fmt"    "sync/atomic"    "time")func main() {    ch := make(chan string, 0)    done := make(chan struct{})    go func() {        var i int32        for {            atomic.AddInt32(&i, 1)            select {            case ch <- fmt.Sprintf("%s%d%s", "第", i, "次向通道中写入数据"):            case <-done:                close(ch)                return            }            // select随机抉择满足条件的case,并不按程序,所以打印出的后果,在30几次稳定            time.Sleep(100 * time.Millisecond)        }    }()    go func() {        time.Sleep(3 * time.Second)        done <- struct{}{}    }()    for i := range ch {        fmt.Println("接管到的值: ", i)    }    fmt.Println("完结")}

输入为:

接管到的值:  第1次向通道中写入数据接管到的值:  第2次向通道中写入数据接管到的值:  第3次向通道中写入数据接管到的值:  第4次向通道中写入数据接管到的值:  第5次向通道中写入数据接管到的值:  第6次向通道中写入数据接管到的值:  第7次向通道中写入数据接管到的值:  第8次向通道中写入数据接管到的值:  第9次向通道中写入数据接管到的值:  第10次向通道中写入数据接管到的值:  第11次向通道中写入数据接管到的值:  第12次向通道中写入数据接管到的值:  第13次向通道中写入数据接管到的值:  第14次向通道中写入数据接管到的值:  第15次向通道中写入数据接管到的值:  第16次向通道中写入数据接管到的值:  第17次向通道中写入数据接管到的值:  第18次向通道中写入数据接管到的值:  第19次向通道中写入数据接管到的值:  第20次向通道中写入数据接管到的值:  第21次向通道中写入数据接管到的值:  第22次向通道中写入数据接管到的值:  第23次向通道中写入数据接管到的值:  第24次向通道中写入数据接管到的值:  第25次向通道中写入数据接管到的值:  第26次向通道中写入数据接管到的值:  第27次向通道中写入数据接管到的值:  第28次向通道中写入数据接管到的值:  第29次向通道中写入数据接管到的值:  第30次向通道中写入数据接管到的值:  第31次向通道中写入数据完结

每次执行,打印出的后果,在30几次稳定

本文由mdnice多平台公布