有没有办法在一个 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()

使用通道实现超时

Source

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()