有关取消的上下文混乱

问题描述 投票:0回答:2
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

我知道

context
在 3 秒后超时,因此当我最后调用
ctx.Err()
时,它确实给了我预期的错误。我还了解到,在我的
myfunc
中,一旦
select
default
的情况匹配,它就不会与
done
匹配。我不明白的是,如何使用上下文逻辑让我的
go func myfunc
在 3 秒内中止。基本上,它不会在 3 秒内终止,所以我想了解 golang 的
ctx
如何帮助我解决这个问题?

go
2个回答
45
投票

如果您想在上下文中使用超时和取消功能,那么在您的情况下需要同步处理

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 seconds 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

示例 #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中调用

您可以在 goroutine 中的

defer
语句上使用
cancel()
(如果需要)。

// ...

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

    // ...
}(ctx)

// ...

30
投票

在您的

for ... select
中,您有 2 个案例:
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
}

现在,您的代码将在遇到

select
时阻塞,而不是进入
default
情况并中断循环。

© www.soinside.com 2019 - 2024. All rights reserved.