【问题标题】:Channel non-determinism using context timeouts, deadlocks使用上下文超时、死锁的通道不确定性
【发布时间】:2021-12-31 07:17:58
【问题描述】:

我正在尝试理解 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!

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

  • 为什么我的 goroutine 没有在一秒钟后结束?
  • 为什么会出现死锁?如何避免这种性质的死锁?

【问题讨论】:

    标签: go deadlock channel goroutine


    【解决方案1】:

    为什么我的 goroutine 不在一秒钟后结束?

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

    limiter.Wait(context.Background())
    

    为什么会出现死锁?如何避免这种性质的死锁?

    这是你的主 goroutine 卡住了。它发生在这里:

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

    没有可以从 msgs 读取的 goroutine,因此它会一直等待。

    这是使其工作的方法之一:

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

    【讨论】:

    • 我能弄明白,但因为你的回答太详细了,我会以你的方式给你一个赞成票。谢谢!
    【解决方案2】:

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

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

    此时程序已导致死锁。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-05-12
      • 2021-08-15
      • 2017-10-05
      • 1970-01-01
      • 1970-01-01
      • 2019-05-04
      • 2019-09-15
      • 2017-06-24
      相关资源
      最近更新 更多