【问题标题】:Go race condition when closing a Go channel关闭 Go 通道时的​​竞争条件
【发布时间】:2018-05-22 16:56:45
【问题描述】:

以下 Go 代码示例在行 c <- byte(0)close(c) 之间有一个竞态条件。使用go test -race 运行代码时会发出信号。

func TestRace(t *testing.T) {
    var c = make(chan byte, 20)
    go func() {
        defer func() {
            if r := recover(); r == nil {
                t.Error("expected panic error")
            }
        }()
        for i := 0; i < 25; i++ {
            c <- byte(0)
        }
        t.Error("expected a panic")
    }()
    close(c)
}

如何避免这种竞争状况?

编辑:根据 Icza 在他的 cmets 中的建议,解决方案如下:

func TestRace(t *testing.T) {
    var c = make(chan byte, 20)
    var done = make(chan struct{})
    go func() {
        for i := 0; i < 25; i++ {
            select{
            case c <- byte(0):
            case <-done:
                close(c)
                return
        }
    }()
    close(done)
}

这不会有竞争条件,并且是干净的。这是一个愚蠢的简单例子。有人告诉我选择会增加开销,但我没有调查它,因为它与我的用例无关。

【问题讨论】:

  • 向上移动 close(c) 以便它在 goroutine 移除竞争条件之前执行。如果出于某种原因,关闭通道和写入通道的责任必须属于不同的 goroutine,那么竞争条件可能总是存在。不过,WaitGroups 和其他锁可能会提供一种安全的方法。

标签: go concurrency race-condition channel goroutine


【解决方案1】:

通常,在通道上发送值的 goroutine 负责关闭它。关闭通道基本上是一个信号,表示将不会(可以)在其上发送更多值。

你没有这样做:你的新 goroutine 是发送值的那个,而你的另一个 goroutine 是关闭它的那个。

要摆脱竞争条件,只需按预期使用通道:将 close(c) 调用移动到在其上发送值的 goroutine,例如:

go func() {
    defer func() {
        if r := recover(); r == nil {
            fmt.Println("expected panic error")
        }
    }()
    for i := 0; i < 25; i++ {
        c <- byte(0)
    }
    close(c)
    fmt.Println("expected a panic")
}()
for x := range c {
    fmt.Println("Received:", x)
}

Go Playground 上试用。

【讨论】:

  • 我会更进一步说接收者关闭频道是错误的; close 在只接收频道上不起作用是有原因的。
  • 我没有看到如何在操场上测试比赛条件。用例是一个控制器,它想要中止管道处理以重新启动它。可能有不止一种数据来源。关闭通道的任何来源都会产生竞争条件。我的示例显示了阻塞的管道。这是否意味着我无法在 Go 中重置管道?
  • @chmike 你不能在操场上“比赛测试”,保存它并在本地运行比赛检测器。如果你想拥有多个源并且有一个“控制器”来重置它,那么你必须协调、同步源和控制器(所有竞争条件的通用解决方案:同步)。
  • @icza 我同意,但频道已经是同步资源。它不应该导致竞争条件。我知道关闭是特殊的,但应该可以关闭一个在写入时阻止 Go 例程的通道。
  • @chmike 有一个简单的解决方案:您可以使用select,其中一种情况是在通道上发送值,另一种情况可能是检查控制器是否启动了“重置” ”。这样,在通道上发送是否会阻塞并不重要。可以通过context.Context 或使用简单的(另一个)频道来广播“重置”。
猜你喜欢
  • 2013-12-25
  • 2020-04-18
  • 1970-01-01
  • 1970-01-01
  • 2023-03-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多