我们如何确保取消上下文会导致 goroutine 的终止?

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

How do we guarantee that a cancelled context will lead to a goroutine's termination?

问题

假设发生以下情况:

  • 我们有下面的Consumer函数,在一个goroutine中运行。

  • 另一个goroutine在intChan通道上发送整数,没有任何延迟。换句话说,在每次for循环迭代时,都有一个值准备好在intChan上接收。

  • 启动Consumer goroutine的goroutine已经取消了传递给Consumer的上下文。因此,ctx.Done()通道也有一个准备好接收的值。

问题:

  • 在这种情况下,select语句的两个case都准备好运行。

  • 根据Go之旅的说法,由于两个case都准备好运行,select会随机选择一个case。

  • 我们如何确保select不会一直选择<- intChan的case?如果在每次for循环迭代中两个case都准备好,我们如何知道最终会选择<- ctx.Done()的case?

func Consumer(ctx context.Context, intChan chan int) {
	for {
		select {
		case <-ctx.Done():
			return
		case i := <-intChan:
			foo(i)
		}
	}
}

我尝试在下面的程序中使用Consumer函数。在运行这个程序的多次运行中,ConsumerProducer goroutine总是终止。

为什么我们不会出现<- ctx.Done()的case从未执行的情况?

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

func main() {

	ctx, cancelFunc := context.WithCancel(context.Background())

	var wg sync.WaitGroup
	wg.Add(2) // 添加2个,因为我们启动了2个goroutine

	Producer(ctx, &wg)

	fmt.Println(time.Now())
	time.Sleep(time.Second * 5) // 5秒后取消上下文

	cancelFunc()
	fmt.Println("CANCELLED")

	wg.Wait() // 等待生产者和消费者goroutine终止
	fmt.Println(time.Now())

}

func Producer(ctx context.Context, wg *sync.WaitGroup) {
	intChan := make(chan int)

	go Consumer(ctx, intChan, wg)

	go func() {
		defer wg.Done()
		for {
			select {
			case <-ctx.Done():
				return
			case intChan <- 1:
			}
		}
	}()

}

func Consumer(ctx context.Context, intChan chan int, wg *sync.WaitGroup) {
	defer wg.Done()

	for {
		select {
		case <-ctx.Done():
			return
		case _ = <-intChan:
		}
	}
}
英文:

Suppose that the following situation occurs:

  • We have the Consumer function below, running in a goroutine.

  • Another goroutine is sending integers on the intChan channel without any delay. In other words, on every iteration of the for-loop, there is a value ready to be received on the intChan.

  • The goroutine that started the Consumer goroutine, has cancelled the context passed into the Consumer. Hence, the ctx.Done() channel also has a value ready to be received.

Question:

  • In this situation, both the cases of the select statement are ready to run.
  • According to the tour of Go, the select will pick one case randomly, since both are ready to run.
  • What's the guarantee that the select won't keep picking the &lt;- intChan case? How do we know that the &lt;- ctx.Done() case will eventually be selected, if both cases are ready in every iteration of the for-loop?
func Consumer(ctx context.Context, intChan chan int) {
	for {
		select {
		case &lt;-ctx.Done():
			return
		case i := &lt;-intChan:
			foo(i)
		}
	}
}

I've tried using the Consumer function, in the program below.
Both the Consumer and Producer goroutines always seem to terminate, in several runs of this program.

Why don't we end up with runs where the &lt;-ctx.Done() case is never executed?

package main

import (
	&quot;context&quot;
	&quot;fmt&quot;
	&quot;sync&quot;
	&quot;time&quot;
)

func main() {

	ctx, cancelFunc := context.WithCancel(context.Background())

	var wg sync.WaitGroup
	wg.Add(2) // add 2, because we spawn 2 goroutines

	Producer(ctx, &amp;wg)

	fmt.Println(time.Now())
	time.Sleep(time.Second * 5) // cancel the context after 5 seconds

	cancelFunc()
	fmt.Println(&quot;CANCELLED&quot;)

	wg.Wait() // wait till both producer and consumer goroutines terminate
	fmt.Println(time.Now())

}

func Producer(ctx context.Context, wg *sync.WaitGroup) {
	intChan := make(chan int)

	go Consumer(ctx, intChan, wg)

	go func() {
		defer wg.Done()
		for {
			select {
			case &lt;-ctx.Done():
				return
			case intChan &lt;- 1:
			}
		}
	}()

}

func Consumer(ctx context.Context, intChan chan int, wg *sync.WaitGroup) {
	defer wg.Done()

	for {
		select {
		case &lt;-ctx.Done():
			return
		case _ = &lt;-intChan:
		}
	}
}

答案1

得分: 1

没有保证。保证终止的最简单方法是在选择语句之外使用ctx.Err()检查错误。将错误返回给传递上下文的代码也是常见的做法。我会像这样编写Consumer函数:

func Consumer(ctx context.Context, intChan chan int) error {
    for ctx.Err() == nil {
        select {
        case <-ctx.Done():
        case i := <-intChan:
            foo(i)
        }
    }
    return ctx.Err()
}
英文:

There is no guarantee. The simplest way to guarantee termination would be to check for error with ctx.Err() outside the select statement. It is also common to return the error back to the code that passed the context. I would write the Consumer func like this:

func Consumer(ctx context.Context, intChan chan int) error {
	for ctx.Err() == nil {
		select {
		case &lt;-ctx.Done():
		case i := &lt;-intChan:
			foo(i)
		}
	}
    return ctx.Err()
}

huangapple
  • 本文由 发表于 2023年7月17日 00:51:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/76699430.html
匿名

发表评论

匿名网友

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

确定