Is there a way to cancel a context after a delay after one goroutine returns?

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

Is there a way to cancel a context after a delay after one goroutine returns?

问题

问题

现状

我目前有一个 gin 处理函数,它在三个独立的 goroutine 中使用相同的上下文运行三个单独的查询。有一个错误组("golang.org/x/sync/errgroup")使用这个共享的上下文,处理程序在返回之前等待错误组。

目标

我试图实现的行为是,在其中一个 goroutine 完成后,剩余的 goroutine 应该强制执行超时,但如果 gin 请求被取消(连接关闭),则还应该取消这个上下文,这意味着需要使用 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 请求上下文是必需的,这样如果连接关闭并且请求被取消,goroutine 也会停止。

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

// 假设 func1 func2 和 func3 都有 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 语言习惯的方法来实现这个行为?

英文:

Problem

Situation

I currently have a gin handler function that runs three separate queries in three separate goroutines using the same context. There is an err group (&quot;golang.org/x/sync/errgroup&quot;) that uses this shared context and the handler waits for the err group before returning.

Objective

The behavior I am trying to implement is after one of the goroutines finishes, there should be a timeout enforced on the remaining goroutines, but also this context should also be cancelled if the gin request is cancelled (connection closed), meaning gin's ctx.Request.Context() would have to be used.

Potential Solutions

Current implementation

Currently, I have a context with timeout passed to an errgroup but this just enforces a timeout for all the goroutines.

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

Using the gin request context is required so that if the connection is closed and the request is cancelled, the goroutines will also stop.

// ctx *gin.Context
g, err := errgroup.WithContext(ctx.Request.Context())

g.Go(func1)
g.Go(func2)
g.Go(func3)

err = g.Wait()

Using a channel to implement timeout

Source

package main

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

func main() {

    c1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c1 &lt;- &quot;result 1&quot;
    }()

    select {
    case res := &lt;-c1:
        fmt.Println(res)
    case &lt;-time.After(1 * time.Second):
        fmt.Println(&quot;timeout 1&quot;)
    }

    c2 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c2 &lt;- &quot;result 2&quot;
    }()
    select {
    case res := &lt;-c2:
        fmt.Println(res)
    case &lt;-time.After(3 * time.Second):
        fmt.Println(&quot;timeout 2&quot;)
    }
}

Combining channels and request context

This solution is close but not very elegant or complete.

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 &lt;- true

if &lt;-cQueryDone {
    select {
        case &lt;-cQueryDone:
            select {
                case &lt;-cQueryDone:
                    // ctx.JSON
                    // return
                case &lt;-time.After(1*time.Second):
                    // ctx.JSON
                    // return
            }
        case &lt;-time.After(3*time.Second):
            // ctx.JSON
            // return
    }
}

err = g.Wait()

Is there a better and more idiomatic way to implement this behavior in Go?

答案1

得分: 2

请注意 context.WithTimeout()

  • 可以包装任何上下文(不仅限于 context.Background()
  • 还会返回一个 cancel 函数

你可以在 ctx.Request.Context() 上添加超时,并在任何查询完成时调用 cancel

timeoutCtx, cancel := context.WithTimeout(ctx.Request.Context())

g, err := errgroup.WithContext(timeoutCtx)

g.Go(func1(cancel)) // 将 cancel 回调传递给每个查询
g.Go(func2(cancel)) // 你可能还想传递 timeoutCtx
g.Go(func3(cancel))

g.Wait()

根据你的评论:还有 context.WithCancel(),你可以在延迟后调用 cancel

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 应该能够访问 hammerTime
g.Go(func2(hammerTime))
g.Go(func3(hammerTime))

g.Wait()
英文:

Note that context.WithTimeout() :

  • can wrap any context (not just context.Background())
  • also returns a cancel function

You can add a timeout on top of ctx.Request.Context(), and call cancel when any of the queries completes :

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

Following your comment : there is also context.WithCancel(), and you can call cancel after a delay

childCtx, cancel := context.WithCancel(ctx.Request.Context())

g, err := errgroup.WithContext(childCtx)

hammerTime := func(){
    &lt;-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()

huangapple
  • 本文由 发表于 2022年4月28日 07:44:05
  • 转载请务必保留本文链接:https://go.coder-hub.com/72036419.html
匿名

发表评论

匿名网友

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

确定