我正在学习如何在 Golang 中使用 context.WithTimeout 在超时后取消长时间运行的操作。这是我的代码的简化版本:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
go func() {
success := make(chan struct{})
f := func() {
// business logic
success <- struct{}{}
}
go f() // problematic point
select {
case <-success:
fmt.Println("Business completed")
case <-ctx.Done():
fmt.Println("Task cancelled")
return
}
}()
这个想法是创建一个超时上下文并在一个单独的 goroutine 中(在最外面的 goroutine 内)处理业务逻辑。我想通过将业务逻辑包装在函数
f
中来响应超时信号。一旦 f
完成,它会发送成功信号,最外层的 goroutine 退出。如果发生超时,最外层的goroutine也应该退出。
问题:
f
,然后等待select
,select
将会被f
阻塞,意味着它无法响应超时信号。select
,然后调用f
,select
会阻塞f
,所以业务逻辑不会执行。f
,最外层的 Goroutine 会响应超时信号并退出,但 f
Goroutine 会继续运行,这就达不到取消的目的。select
创建了一个单独的goroutine,它也只会终止select
块,但是正在运行的f
的goroutine仍然会继续执行。问题:
如何构造代码以便在超时发生时 f 也被取消?
我尝试将业务逻辑(f)放在一个单独的 Goroutine 中,以便最外层的 Goroutine 可以使用 context.WithTimeout 处理超时。我预计如果发生超时,最外面的 goroutine 和 f goroutine 都会被取消。然而,只有最外层的 goroutine 在超时时退出,而 f goroutine 继续运行,这不是期望的行为。
正如其他人已经说过的,你的 goroutine 必须定期合作并检查上下文取消:
f := func() {
// business logic stage 1
if ctx.Err()!=nil {
return
}
// business logic state 2
if ctx.Err()!=nil {
return
}
success <- struct{}{}
}
然后可以选择超时和完成:
select {
case <-success:
fmt.Println("Business completed")
case <-ctx.Done():
fmt.Println("Task cancelled")
return
}
您可能还想等到 goroutine 实际返回后再从外部 goroutine 返回。为此,您可以:
done := make(chan struct{})
f:=func() {
defer close(done)
.... // rest of f()
}
然后做:
select {
case <-success:
fmt.Println("Business completed")
case <-ctx.Done():
fmt.Println("Task cancelled")
<-done
return
}