关于后端:contextWithCancel的使用

41次阅读

共计 7778 个字符,预计需要花费 20 分钟才能阅读完成。

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

Go 语言 context 包 -cancelCtx

疑难

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

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

package main

import (
    "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 main

import (
    "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 main

import (
    "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 main

import (
    "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 main

import (
    "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 多平台公布

正文完
 0