有没有办法在一个 goroutine returns 延迟后取消上下文?
Is there a way to cancel a context after a delay after one goroutine returns?
问题
情况
我目前有一个 gin 处理函数,它使用相同的上下文在三个独立的 goroutine 中运行三个独立的查询。有一个使用此共享上下文的错误组 ("golang.org/x/sync/errgroup"
),处理程序在返回之前等待错误组。
Objective
我试图实现的行为是在一个 goroutine 完成后,应该对剩余的 goroutine 强制执行超时,但如果 gin 请求被取消(连接关闭),这个上下文也应该被取消,这意味着必须使用杜松子酒的 ctx.Request.Context()
。
可能的解决方案
当前实施
目前,我有一个超时传递给错误组的上下文,但这只是对所有 goroutine 强制执行超时。
timeoutCtx := context.WithTimeout(context.Background(), 10*time.Second)
g, err := errgroup.WithContext(timeoutCtx)
g.Go(func1)
g.Go(func2)
g.Go(func3)
err = g.Wait()
需要使用 gin 请求上下文,这样如果连接关闭并且请求被取消,goroutines 也会停止。
// ctx *gin.Context
g, err := errgroup.WithContext(ctx.Request.Context())
g.Go(func1)
g.Go(func2)
g.Go(func3)
err = g.Wait()
使用通道实现超时
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout 1")
}
c2 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c2 <- "result 2"
}()
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(3 * time.Second):
fmt.Println("timeout 2")
}
}
组合渠道和请求上下文
这个解决方案很接近,但不是很优雅或完整。
cQueryDone := make(chan bool)
g, err := errgroup.WithContext(ctx.Request.Context())
g.Go(func1)
g.Go(func2)
g.Go(func3)
// assumes func1 func2 and func3 all have cQueryDone <- true
if <-cQueryDone {
select {
case <-cQueryDone:
select {
case <-cQueryDone:
// ctx.JSON
// return
case <-time.After(1*time.Second):
// ctx.JSON
// return
}
case <-time.After(3*time.Second):
// ctx.JSON
// return
}
}
err = g.Wait()
在 Go 中是否有更好、更惯用的方法来实现此行为?
注意 context.WithTimeout()
:
- 可以包装任何上下文(不仅仅是
context.Background()
)
- 还有returns一个
cancel
函数
您可以在 ctx.Request.Context()
之上添加超时,并在任何查询完成时调用 cancel
:
timeoutCtx, cancel := context.WithTimeout(ctx.Request.Context())
g, err := errgroup.WithContext(timeoutCtx)
g.Go( func1(cancel) ) // pass the cancel callback to each query some way or another
g.Go( func2(cancel) ) // you prabably want to also pass timeoutCtx
g.Go( func3(cancel) )
g.Wait()
根据您的评论:还有context.WithCancel()
,您可以在延迟后调用取消
childCtx, cancel := context.WithCancel(ctx.Request.Context())
g, err := errgroup.WithContext(childCtx)
hammerTime := func(){
<-time.After(1*time.Second)
cancel()
}
g.Go( func1(hammerTime) ) // funcXX should have access to hammerTime
g.Go( func2(hammerTime) )
g.Go( func3(hammerTime) )
g.Wait()
问题
情况
我目前有一个 gin 处理函数,它使用相同的上下文在三个独立的 goroutine 中运行三个独立的查询。有一个使用此共享上下文的错误组 ("golang.org/x/sync/errgroup"
),处理程序在返回之前等待错误组。
Objective
我试图实现的行为是在一个 goroutine 完成后,应该对剩余的 goroutine 强制执行超时,但如果 gin 请求被取消(连接关闭),这个上下文也应该被取消,这意味着必须使用杜松子酒的 ctx.Request.Context()
。
可能的解决方案
当前实施
目前,我有一个超时传递给错误组的上下文,但这只是对所有 goroutine 强制执行超时。
timeoutCtx := context.WithTimeout(context.Background(), 10*time.Second)
g, err := errgroup.WithContext(timeoutCtx)
g.Go(func1)
g.Go(func2)
g.Go(func3)
err = g.Wait()
需要使用 gin 请求上下文,这样如果连接关闭并且请求被取消,goroutines 也会停止。
// ctx *gin.Context
g, err := errgroup.WithContext(ctx.Request.Context())
g.Go(func1)
g.Go(func2)
g.Go(func3)
err = g.Wait()
使用通道实现超时
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout 1")
}
c2 := make(chan string, 1)
go func() {
time.Sleep(2 * time.Second)
c2 <- "result 2"
}()
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(3 * time.Second):
fmt.Println("timeout 2")
}
}
组合渠道和请求上下文
这个解决方案很接近,但不是很优雅或完整。
cQueryDone := make(chan bool)
g, err := errgroup.WithContext(ctx.Request.Context())
g.Go(func1)
g.Go(func2)
g.Go(func3)
// assumes func1 func2 and func3 all have cQueryDone <- true
if <-cQueryDone {
select {
case <-cQueryDone:
select {
case <-cQueryDone:
// ctx.JSON
// return
case <-time.After(1*time.Second):
// ctx.JSON
// return
}
case <-time.After(3*time.Second):
// ctx.JSON
// return
}
}
err = g.Wait()
在 Go 中是否有更好、更惯用的方法来实现此行为?
注意 context.WithTimeout()
:
- 可以包装任何上下文(不仅仅是
context.Background()
) - 还有returns一个
cancel
函数
您可以在 ctx.Request.Context()
之上添加超时,并在任何查询完成时调用 cancel
:
timeoutCtx, cancel := context.WithTimeout(ctx.Request.Context())
g, err := errgroup.WithContext(timeoutCtx)
g.Go( func1(cancel) ) // pass the cancel callback to each query some way or another
g.Go( func2(cancel) ) // you prabably want to also pass timeoutCtx
g.Go( func3(cancel) )
g.Wait()
根据您的评论:还有context.WithCancel()
,您可以在延迟后调用取消
childCtx, cancel := context.WithCancel(ctx.Request.Context())
g, err := errgroup.WithContext(childCtx)
hammerTime := func(){
<-time.After(1*time.Second)
cancel()
}
g.Go( func1(hammerTime) ) // funcXX should have access to hammerTime
g.Go( func2(hammerTime) )
g.Go( func3(hammerTime) )
g.Wait()