使用上下文超时、死锁的通道不确定性

Channel non-determinism using context timeouts, deadlocks

我试图理解 Go 中的上下文和通道,但我无法理解正在发生的事情。这是一些示例代码。

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "golang.org/x/time/rate"
)

func main() {
    msgs := make(chan string)

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

    limiter := rate.NewLimiter(rate.Every(2*time.Second), 1)

    go func(ctx context.Context, limiter *rate.Limiter) {
        for {
            limiter.Wait(context.Background())

            select {
            case <-ctx.Done():
                log.Printf("finished!")
                return
            case msg := <-msgs:
                log.Printf("receiving a message: %s", msg)
            }
        }
    }(ctx, limiter)

    defer close(msgs)

    i := 0
    for {
        msgs <- fmt.Sprintf("sending message %d", i)
        i++
        if i > 10 {
            break
        }
    }
}

我得到的结果是不确定的。有时记录器打印出三条消息,有时是五条。此外,程序每次都以死锁结束:

2021/12/31 02:07:21 receiving a message: sending message 0
2021/12/31 02:07:23 receiving a message: sending message 1
2021/12/31 02:07:25 receiving a message: sending message 2
2021/12/31 02:07:27 receiving a message: sending message 3
2021/12/31 02:07:29 receiving a message: sending message 4
2021/12/31 02:07:29 finished!
fatal error: all goroutines are asleep - deadlock!

所以,我想我有几个问题:

Why doesn't my goroutine simply end after one second?

虽然 goroutine 可能会在这里等待而不是 select:

limiter.Wait(context.Background())

Why is there a deadlock? How can I avoid deadlocks of this nature?

卡住的是您的主协程。它发生在这里:

msgs <- fmt.Sprintf("sending message %d", I)

没有可以从 msgs 读取的 goroutines,所以它会一直等待。

以下是实现它的方法之一:

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "golang.org/x/time/rate"
)

func main() {
    msgs := make(chan string)

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

    limiter := rate.NewLimiter(rate.Every(1*time.Second), 1)

    go func() {
        for {
            limiter.Wait(context.Background())

            select {
            case <-ctx.Done():
                log.Printf("finished!")
                return
            case msg := <-msgs:
                log.Printf("receiving a message: %s", msg)
            }
        }
    }()

    defer close(msgs)

    for i := 0; i < 100000; i++ {
        select {
        case msgs <- fmt.Sprintf("sending message %d", i):
        case <-ctx.Done():
            log.Printf("finished too!")
            return
        }
    }
}

使用<-发送或接收消息是一个阻塞操作。当上下文超时结束时,goroutine 现在已经退出,调用者无法继续。

// Goroutine has finished, this call will never finish
msgs <- fmt.Sprintf("sending message %d", i)

此时程序出现了死锁。

至于非确定性,goroutine和主执行上下文运行并发。因为有两个 for 循环,没有太多延迟,所以线程在相互竞争。不能保证它们每次都会以相同的方式执行。