Context confusion regarding cancellation

cur*_*eer 9 go

package main

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

func myfunc(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Ctx is kicking in with error:%+v\n", ctx.Err())
            return
        default:
            time.Sleep(15 * time.Second)
            fmt.Printf("I was not canceled\n")
            return
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(
        context.Background(),
        time.Duration(3*time.Second))
    defer cancel()

    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        myfunc(ctx)
    }()

    wg.Wait()
    fmt.Printf("In main, ctx err is %+v\n", ctx.Err())
}
Run Code Online (Sandbox Code Playgroud)

I have the above snippet that does print the output like this

I was not canceled
In main, ctx err is context deadline exceeded

Process finished with exit code 0
Run Code Online (Sandbox Code Playgroud)

I understand that context times-out after 3 seconds and hence it does give me the expected error when I call ctx.Err() in the end. I also get the fact that in my myfunc once select matches on the case for default, it won't match on the done. What I do not understand is that how do I make my go func myfunc get aborted in 3 seconds using the context logic. Basically, it won't terminate in 3 seconds so I am trying to understand how can golang's ctx help me with this?

nov*_*ung 20

如果你要使用的超时和取消功能从上下文,然后在你的情况ctx.Done()需要进行同步处理。

来自https://golang.org/pkg/context/#Context 的解释

Done 返回一个在代表此上下文完成工作时关闭的通道应该被取消。如果此上下文永远无法取消,则 Done 可能返回 nil。对 Done 的连续调用返回相同的值。

所以基本上<-ctx.Done()将在两个条件下调用:

  1. 当上下文超时超过
  2. 当上下文被强制取消时

当这种情况发生时,ctx.Err()将永远不会nil

我们可以对错误对象进行一些检查,以查看上下文是否被强制取消或超过超时。

Context 包提供了两个错误对象,context.DeadlineExceeded以及context.Timeout,这两个将帮助我们识别为什么<-ctx.Done()被调用。


示例 #1 场景:上下文被强制取消(通过cancel()

在测试中,我们将尝试在超时之前取消上下文,以便<-ctx.Done()执行。

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Duration(3*time.Second))

go func(ctx context.Context) {
    // simulate a process that takes 2 second to complete
    time.Sleep(2 * time.Second)

    // cancel context by force, assuming the whole process is complete
    cancel()
}(ctx)

select {
case <-ctx.Done():
    switch ctx.Err() {
    case context.DeadlineExceeded:
        fmt.Println("context timeout exceeded")
    case context.Canceled:
        fmt.Println("context cancelled by force. whole process is complete")
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

$ go run test.go 
context cancelled by force
Run Code Online (Sandbox Code Playgroud)

示例 #2 场景:超出上下文超时

在这种情况下,我们使进程花费的时间比上下文超时长,因此理想情况下<-ctx.Done()也将执行。

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Duration(3*time.Second))

go func(ctx context.Context) {
    // simulate a process that takes 4 second to complete
    time.Sleep(4 * time.Second)

    // cancel context by force, assuming the whole process is complete
    cancel()
}(ctx)

select {
case <-ctx.Done():
    switch ctx.Err() {
    case context.DeadlineExceeded:
        fmt.Println("context timeout exceeded")
    case context.Canceled:
        fmt.Println("context cancelled by force. whole process is complete")
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

$ go run test.go 
context timeout exceeded
Run Code Online (Sandbox Code Playgroud)

示例 #3 场景:由于发生错误而强制取消上下文

可能会出现这样的情况,因为发生了错误,我们需要在过程中停止 goroutine。有时,我们可能需要在主程序中检索该错误对象。

为了实现这一点,我们需要一个额外的通道来将错误对象从 goroutine 传输到主程序中。

在下面的示例中,我准备了一个名为chErr. 每当(goroutine)进程中间发生错误时,我们将通过通道发送该错误对象,然后立即停止进程。

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Duration(3*time.Second))

chErr := make(chan error)

go func(ctx context.Context) {
    // ... some process ...

    if err != nil {
        // cancel context by force, an error occurred
        chErr <- err
        return
    }

    // ... some other process ...

    // cancel context by force, assuming the whole process is complete
    cancel()
}(ctx)

select {
case <-ctx.Done():
    switch ctx.Err() {
    case context.DeadlineExceeded:
        fmt.Println("context timeout exceeded")
    case context.Canceled:
        fmt.Println("context cancelled by force. whole process is complete")
    }
case err := <-chErr:
    fmt.Println("process fail causing by some error:", err.Error())
}
Run Code Online (Sandbox Code Playgroud)

附加信息 #1:cancel()在上下文初始化后立即调用

根据有关该功能的上下文文档cancel()

取消此上下文会释放与其关联的资源,因此一旦此上下文中运行的操作完成,代码应立即调用取消。

总是cancel()在上下文声明之后立即调用函数是很好的。它是否也在 goroutine 中被调用并不重要。这是因为当块内的整个过程完全完成时,确保上下文总是被取消。

ctx, cancel := context.WithTimeout(
    context.Background(),
    time.Duration(3*time.Second))
defer cancel()

// ...
Run Code Online (Sandbox Code Playgroud)

附加信息 #2:defer cancel()在 goroutine 中调用

您可以在 goroutine 中defercancel()语句上使用(如果需要)。

// ...

go func(ctx context.Context) {
    defer cancel()

    // ...
}(ctx)

// ...
Run Code Online (Sandbox Code Playgroud)


Dom*_*nes 14

在您的 中for ... select,您有两种情况:case <-ctx.Done():default:。当您的代码到达 时select,它会进入这种default情况,因为上下文尚未取消,它会休眠 15 秒然后返回,从而中断您的循环。(换句话说,它不会阻止/等待您的上下文取消)

如果您希望您的代码执行您所描述的操作,您需要select有上下文被取消强制超时的情况。

select {
case <-ctx.Done(): // context was cancelled
  fmt.Printf("Ctx is kicking in with error:%+v\n", ctx.Err())
  return
case <-time.After(15 * time.Second): // 15 seconds have elapsed
  fmt.Printf("I was not canceled\n")
  return
}
Run Code Online (Sandbox Code Playgroud)

现在,您的代码将在遇到 时阻塞select,而不是进入default案例并打破循环。

  • 这个答案实际上指出了根本原因。 (2认同)
  • 这不是一个实用的函数实现。您正在使用“time.After”来表示“长任务”。在实际场景中,我们如何实现一个实际运行长时间任务并在达到超时后中止该函数的函数? (2认同)