首页 > 解决方案 > 关于取消的上下文混淆

问题描述

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

我有上面的片段可以打印这样的输出

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

Process finished with exit code 0

我知道context3 秒后超时,因此当我ctx.Err()最后打电话时它确实给了我预期的错误。我还得到这样一个事实,即在我myfunc曾经select匹配的情况下default,它不会匹配done. 我不明白的是,如何go func myfunc使用上下文逻辑让我在 3 秒内中止。基本上,它不会在 3 秒内终止,所以我试图了解 golang 如何ctx帮助我解决这个问题?

标签: go

解决方案


如果您想使用上下文中的超时和取消功能,那么在您的情况下需要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")
    }
}

输出:

$ go run test.go 
context cancelled by force

示例 #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")
    }
}

输出:

$ go run test.go 
context timeout exceeded

Example #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())
}

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

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

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

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

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

// ...

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

您可以在 goroutinedefer中的cancel()语句上使用(如果需要)。

// ...

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

    // ...
}(ctx)

// ...

推荐阅读