为什么总是不推荐跳过调用子上下文的取消函数?

huangapple go评论125阅读模式
英文:

Why is it always bad practice to skip calling the cancel function for a child context?

问题

问题描述

在使用Go语言中的"context"包时,如果你没有调用context.WithCancel()返回的cancel函数,"go vet"命令的"lostcancel"检查将会给出警告。例如,如果你有以下代码:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    foo(ctx)
    fmt.Println("done")
}

...那么Go vet会给出以下警告:
> the cancel function is not used on all paths (possible context leak)

这也在Context包的文档中提到:
> Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires. The go vet tool checks that CancelFuncs are used on all control-flow paths.

我的问题是:至少在某种情况下,我不明白为什么必须调用cancel函数。假设我想派生一个子上下文,以便我可以选择:

  1. 在父上下文被取消之前,使用子上下文停止下游的goroutine,例如在遇到早期错误时,或者
  2. 等待父上下文被取消,让子上下文的生命周期与父上下文一样长。

在我看来,这是"context"包的父/子树结构的一个非常自然的用例。但是,这会导致与上述相同的go vet错误,因为在代表第二个选项(让子上下文的生命周期与父上下文相同)的代码执行路径中,子上下文的cancel函数不会被调用。

那么,这是否真的是go vet/Go社区在使用Context时的指南的不足,还是我对这个问题的思考有缺陷?

示例程序

下面是一个出现错误的示例程序。Go vet会对barctx, barcancel := context.WithCancel(ctx)这一行给出警告。这个示例显然很愚蠢,因为我可以在启动它的goroutine之前就检查bar是否有错误。但是可以说:在我的实际程序中,这不是一个选项。

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    foo(ctx)
    cancel()

    time.Sleep(10 * time.Millisecond)
    fmt.Println("done")
}

func foo(ctx context.Context) {
    go func() {
        <-ctx.Done()
	    fmt.Println("foo done")
    }()

    for i := 0; i < 2; i++ {
	    barctx, barcancel := context.WithCancel(ctx)
	    err := bar(barctx, i)
	    if err != nil {
	        fmt.Println("bar error; canceled")
	        barcancel()
	    }
    }
}

func bar(ctx context.Context, x int) (err error) {
    go func() {
	    <-ctx.Done()
	    fmt.Printf("bar done; x=%d\n", x)
    }()

    if x > 0 {
	    err = fmt.Errorf("bar error")
    }
    return err
}
英文:

Problem Description

When using the "context" package in Go, the go vet command's "lostcancel" check will give a warning if you don't call the cancel function returned by context.WithCancel(). e.g. if you have:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    foo(ctx)
    fmt.Println(&quot;done&quot;)
}

...then Go vet would give you the warning:
> the cancel function is not used on all paths (possible context leak)

This is also mentioned in the Context package's documentation:
> Failing to call the CancelFunc leaks the child and its children until the parent is canceled or the timer fires. The go vet tool checks that CancelFuncs are used on all control-flow paths.

My issue is this: in at least one instance, I don't see why the cancel function should have to be called. Suppose I want to derive a child context so that I have the option to either:

  1. stop downstream goroutines using the child context before the parent is canceled, such as upon encountering an early error, or
  2. wait until the parent context is canceled, letting the child context live as long as the parent.

This seems to me to be a very natural use-case of the context package's parent/child tree structure. But this gives the same go vet error as mentioned above, because in the code execution path representing the second option (letting the child live for the same duration as the parent), the child context's cancel function wouldn't be called.

So, is this truly a shortcoming of go vet/the Go community's guidelines for using Context, or is there a flaw in my thinking about this topic?

Example Program

Here is a sample program where the error occurs. Go vet would give the warning for the line barctx, barcancel := context.WithCancel(ctx). This example is obviously silly, because I could just check bar for an error before starting up its goroutine. But suffice it to say: in my actual program, this would not be an option.

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    foo(ctx)
    cancel()

    time.Sleep(10 * time.Millisecond)
    fmt.Println(&quot;done&quot;)
}

func foo(ctx context.Context) {
    go func() {
        &lt;-ctx.Done()
	    fmt.Println(&quot;foo done&quot;)
    }()

    for i := 0; i &lt; 2; i++ {
	    barctx, barcancel := context.WithCancel(ctx)
	    err := bar(barctx, i)
	    if err != nil {
	        fmt.Println(&quot;bar error; canceled&quot;)
	        barcancel()
	    }
    }
}

func bar(ctx context.Context, x int) (err error) {
    go func() {
	    &lt;-ctx.Done()
	    fmt.Printf(&quot;bar done; x=%d\n&quot;, x)
    }()

    if x &gt; 0 {
	    err = fmt.Errorf(&quot;bar error&quot;)
    }
    return err
}

答案1

得分: 0

在你的示例中,go vet 返回警告是正确的。问题具体出在 bar 函数上。当你创建它时,bar 创建了一个 goroutine,该 goroutine 会阻塞直到 barcancelcancel 被调用。在此之前,它将继续存在。因此,你必须调用 cancel 函数,以便等待上下文完成的资源可以超出范围并进行垃圾回收。

明确一点,在你的示例中,所有的延迟例程都将被取消,因为你取消了 ctx,这也取消了每个 barctx 实例。然而,你不应该依赖这一点,因为你可能无法控制父上下文是否会被取消。因此,你的函数应该对它创建和在函数调用中使用的上下文负责。

明确一点,你应该像这样修改你的示例代码:

barctx, barcancel := context.WithCancel(ctx)
err := bar(barctx, i)
if err != nil {
    fmt.Println("bar error; canceled")
}

barcancel()
英文:

In your example, go vet would be correct to return a warning. The problem specifically lies in the bar function. When you create it, bar creates a goroutine that blocks until barcancel or cancel is called. Until then, it will continue to exist. Therefore, it is imperative that you call the cancel function so that resources waiting for the context to be done can go out of scope and be garbage-collected.

To be clear, in your example, all the deferred routines will be cancelled because you cancelled ctx, which also cancelled every instance of barctx. However, you shouldn't depend on this as you might not have control over whether or not the parent context will be cancelled. So, your function should take responsibility for the contexts it creates and uses in function invocations.

To be clear, you should modify your example code like this:

barctx, barcancel := context.WithCancel(ctx)
err := bar(barctx, i)
if err != nil {
    fmt.Println(&quot;bar error; canceled&quot;)
}

barcancel()

huangapple
  • 本文由 发表于 2022年11月8日 08:28:47
  • 转载请务必保留本文链接:https://go.coder-hub.com/74354475.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定