为啥下面的代码会产生死锁

Posted

技术标签:

【中文标题】为啥下面的代码会产生死锁【英文标题】:Why following code generates deadlock为什么下面的代码会产生死锁 【发布时间】:2017-07-09 06:00:43 【问题描述】:

Golang 新手在这里。有人能解释一下为什么下面的代码会产生死锁吗?

我知道将 true 发送到 boolean

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg2 sync.WaitGroup

func producer2(c chan<- int) 
    for i := 0; i < 5; i++ 
        time.Sleep(time.Second * 10)
        fmt.Println("Producer Writing to chan %d", i)
        c <- i
    


func consumer2(c <-chan int) 
    defer wg2.Done()
    fmt.Println("Consumer Got value %d", <-c)



func main() 
    c := make(chan int)
    wg2.Add(5)
    fmt.Println("Starting .... 1")
    go producer2(c)
    go consumer2(c)
    fmt.Println("Starting .... 2")

    wg2.Wait()

以下是我的理解,我知道这是错误的:

    通道将在写入 0 时被阻塞 生产者函数循环 所以我希望通道被 之后的消费者。 由于通道在步骤 2 中被清空, 生产者函数可以再次输入另一个值然后得到 被阻止并再次重复第 2 步。

【问题讨论】:

您向频道发送了五次消息,但只使用了一次频道。 producer2 goroutine 将被阻塞,直到四个消息的其余部分被消费。 另外,您对sync.WaitGroup 的使用是错误的,因为它调用了Add(5),但只调用了一次Done() @ymonad 我补充了我的理解。你能纠正我哪里出错了吗?注册。 Add(5) - 当消费者执行 5 次时,我期望 Done() 被调用 5 次 在第 3 步中,您提到第 2 步会再次重复,但事实并非如此,您的代码中的消费者只消费一次。 【参考方案1】:

你原来的死锁是由wg2.Add(5)引起的,你在等待5个goroutines完成,但只有一个完成;你打过一次wg2.Done()。把它改成wg2.Add(1),你的程序就可以正常运行了。

但是,我怀疑您打算使用通道中的所有值,而不仅仅是像您所做的那样。如果您将消费者功能更改为:

func consumer2(c <-chan int) 
    defer wg2.Done()
    for i := range c 
        fmt.Printf("Consumer Got value %d\n", i)
    

你会遇到另一个死锁,因为通道没有在生产者函数中关闭,而消费者正在等待更多永远不会到达的值。将close(c) 添加到生产者函数将修复它。

【讨论】:

【参考方案2】:

为什么会出错?

运行您的代码得到以下错误:

➜  gochannel go run dl.go
Starting .... 1
Starting .... 2
Producer Writing to chan 0
Consumer Got value 0
Producer Writing to chan 1
fatal error: all goroutines are asleep - deadlock!

原因如下:

您的代码中有三个 goroutine:mainproducer2consumer2。运行时,

producer2 向频道发送号码0 consumer2 从频道接收 0,然后退出 producer21 发送到频道,但没有人在消费,因为consumer2 已经退出 producer2 正在等待 main 执行 wg2.Wait(),但并非所有等待组都已关闭。所以 ma​​in 正在等待

有两个goroutines在这里等待,什么都不做,不管你等多久都不会做任何事情。 这是一个死锁! Golang 检测到它并恐慌。

您在这里混淆了两个概念:

    waitgourp 的工作原理 如何从通道接收所有值

我在这里简单解释一下,网上已经有很多文章了。

等待组的工作原理

WaitGroup 如果有一种方法可以等待所有 groutine 完成。在后台运行 goroutine 时,重要的是要知道它们何时全部退出,然后才能执行某些操作。

在你的例子中,我们运行了两个goroutine,所以一开始我们应该设置wg2.Add(2),每个goroutine应该添加wg2.Done()来通知它已经完成。

从通道接收数据

从通道接收数据时。如果您确切知道它将发送多少数据,请以这种方式使用for 循环:

for i:=0; i<N; i++ 
    data = <-c
    process(data)

否则这样使用:

for data := range c 
    process(data)

另外,不要忘记在没有更多数据发送时关闭通道。

如何解决?

通过上面的解释,代码可以固定为:

package main

import (
    "fmt"
    "sync"
    "time"
)

var wg2 sync.WaitGroup

func producer2(c chan<- int) 
    defer wg2.Done()
    for i := 0; i < 5; i++ 
        time.Sleep(time.Second * 1)
        fmt.Printf("Producer Writing to chan %d\n", i)
        c <- i
    
    close(c)


func consumer2(c <-chan int) 
    defer wg2.Done()
    for i := range c 
        fmt.Printf("Consumer Got value %d\n", i)
    



func main() 
    c := make(chan int)
    wg2.Add(2)
    fmt.Println("Starting .... 1")
    go producer2(c)
    go consumer2(c)
    fmt.Println("Starting .... 2")

    wg2.Wait()

Here 是另一种可能的修复方法。

预期输出

固定代码给出以下输出:

➜  gochannel go run dl.go
Starting .... 1
Starting .... 2
Producer Writing to chan 0
Consumer Got value 0
Producer Writing to chan 1
Consumer Got value 1
Producer Writing to chan 2
Consumer Got value 2
Producer Writing to chan 3
Consumer Got value 3
Producer Writing to chan 4
Consumer Got value 4

【讨论】:

以上是关于为啥下面的代码会产生死锁的主要内容,如果未能解决你的问题,请参考以下文章

为啥这个事务会产生死锁?

C# async await 死锁问题总结

为啥将 std::mutex 引入成员类会产生此编译错误?

当 Hangfire 并行处理多个作业时,为啥 MySQL InnoDB 会产生如此多的死锁?

这些代码使用 Rust Dashmap 会产生死锁吗?

数据库中死锁是啥产生的?