为了账号安全,请及时绑定邮箱和手机立即绑定

为什么在 defer 语句中关闭通道会出现恐慌?

为什么在 defer 语句中关闭通道会出现恐慌?

Go
HUH函数 2023-07-31 10:50:34
在下面的示例中,go 例程将值泵入无缓冲通道,并且 main 函数对其进行迭代。package mainimport (    "fmt"    "strconv")var chanStr chan stringfunc main() {    go pump()    fmt.Println("iterating ...")    for val := range chanStr {        fmt.Printf("fetched val: %s from channel\n", val)    }}func pump() {    defer close(chanStr)    chanStr = make(chan string)    for i := 1; i <= 5; i++ {        fmt.Printf("pumping seq %d into channel\n", i)        chanStr <- "val" + strconv.Itoa(i)    }    //close(chanStr)}该函数会出现恐慌并输出以下内容:iterating ...                                             pumping seq 1 into channel                                pumping seq 2 into channel                                fetched val: val1 from channel                            ......fetched val: val4 from channel                            pumping seq 5 into channel                                panic: close of nil channel                               goroutine 5 [running]:                                    main.pump()                                                       C:/personal/gospace/go-rules/test.go:26 +0x1a6    created by main.main                                              C:/personal/gospace/go-rules/test.go:11 +0x4e     但是,如果我评论 defer 语句并在 goroutine 中的 for 循环之后立即关闭pump,接收器不会惊慌。 这两种情况有什么区别?看起来 defer 在收到值之前关闭通道,但常规关闭会等待。此外,当我使用竞争检测器进行构建时,即使在常规关闭中,它也会标记潜在的竞争条件(我无法每次都重新创建竞争)。这是否意味着这两种方式都不能优雅地关闭通道?更新: 对于所有评论,我知道问题是什么。我必须在函数的第一行创建通道main()。不过,我在 Windows 上运行 go1.12,并且观察到了这种行为。显然我没有伪造输出。我一直使用 defer 语句重新创建恐慌,甚至在 for 循环后立即关闭通道时也没有发生过恐慌pump()
查看完整描述

3 回答

?
梵蒂冈之花

TA贡献1900条经验 获得超5个赞

您的代码在不同方面都非常活泼:


for val在 goroutine 实际初始化通道之前,您有可能(事实上,很有可能)开始从循环中的通道读取数据,从而导致死锁。


iterating ...

pumping seq 1 into channel

fatal error: all goroutines are asleep - deadlock!

事实上,这是我观察到的在本地或操场上按原样执行代码的唯一行为。


如果我添加一个延迟,


 fmt.Println("iterating ...")

 time.Sleep(10 * time.Millisecond) // Delay ensures the channel has been created

 for val := range chanStr {

然后我确实观察了您注意到的行为:


iterating ...

pumping seq 1 into channel

fetched val: val1 from channel

pumping seq 2 into channel

pumping seq 3 into channel

fetched val: val2 from channel

fetched val: val3 from channel

pumping seq 4 into channel

pumping seq 5 into channel

fetched val: val4 from channel

fetched val: val5 from channel

panic: close of nil channel

原因是你在调用close(chanStr)时chanStr仍然为零。defer如果您在创建频道后致电您:


func pump() {

    chanStr = make(chan string)

    defer close(chanStr)

你会解决这个问题的。


要解决这两个竞争,您需要在调用 goroutine之前初始化通道。完整代码:


package main


import (

    "fmt"

    "strconv"

)


var chanStr chan string


func main() {

    chanStr = make(chan string)

    go pump(chanStr)

    fmt.Println("iterating ...")

    for val := range chanStr {

        fmt.Printf("fetched val: %s from channel\n", val)

    }

}


func pump(chanStr chan string) {

    defer close(chanStr)

    for i := 1; i <= 5; i++ {

        fmt.Printf("pumping seq %d into channel\n", i)

        chanStr <- "val" + strconv.Itoa(i)

    }

}

为了进一步说明问题是立即defer close(chanStr)评估chanStr(当它仍然是nil)时,请考虑这个(不推荐!)替代解决方案:


package main


import (

    "fmt"

    "strconv"

    "time"

)


var chanStr chan string


func main() {

    go pump()

    fmt.Println("iterating ...")

    time.Sleep(10 * time.Millisecond)

    for val := range chanStr {

        fmt.Printf("fetched val: %s from channel\n", val)

    }

}


func pump() {

    defer func() {

        close(chanStr)

    }()

    chanStr = make(chan string)

    for i := 1; i <= 5; i++ {

        fmt.Printf("pumping seq %d into channel\n", i)

        chanStr <- "val" + strconv.Itoa(i)

    }

}

在这种情况下,延迟函数是 的闭包chanStr,因此chanStr的计算被延迟到实际执行。在此版本中,当延迟函数执行时,chanStr不再为 nil,因此无需恐慌。


查看完整回答
反对 回复 2023-07-31
?
手掌心

TA贡献1942条经验 获得超3个赞

主 go 例程有可能在创建通道之前从通道中读取数据。这就是你的数据竞赛。

应在开始您的 go 例程之前创建通道。

修复: https: //play.golang.org/p/O7pgM05KEtI


查看完整回答
反对 回复 2023-07-31
?
慕标5832272

TA贡献1966条经验 获得超4个赞

您发布的代码存在死锁情况。您可能没有发布相同的代码。


这是应该可以工作的更新后的代码:


package main


import (

    "fmt"

    "strconv"

)


func main() {


    chanStr := make(chan string)


    go pump(chanStr)

    fmt.Println("iterating ...")

    for val := range chanStr {

        fmt.Printf("fetched val: %s from channel\n", val)

    }

}


func pump(ch chan string) {

    defer close(ch)


    for i := 1; i <= 5; i++ {

        fmt.Printf("pumping seq %d into channel\n", i)

        ch <- "val" + strconv.Itoa(i)

    }

    //close(chanStr)

}


本例中的问题是,您在函数内部创建了通道pump,因此主函数不知道如何使用数据,并且由于没有使用者而导致死锁。


查看完整回答
反对 回复 2023-07-31
  • 3 回答
  • 0 关注
  • 182 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信