解决goroutines僵局

时间:2018-02-19 11:48:14

标签: go concurrency channel goroutine

我一直试图解决我在Golang并发中遇到的这个简单问题。我一直在寻找所有可能的解决方案,但没有发现我的问题(或者我可能会错过一个)。这是我的代码:

package main

import (
    "fmt"
    "time"
)

func producer(ch chan int, d time.Duration, num int) {

    for i:=0; i<num; i++ {
        ch <- i
        time.Sleep(d)
    }
}

func main() {
    ch := make(chan int)

    go producer(ch, 100*time.Millisecond, 2)
    go producer(ch, 200*time.Millisecond, 5)

    for {
        fmt.Println(<-ch)    
    }

    close(ch)
}

打印错误:

  

致命错误:所有goroutine都睡着了 - 死锁!

     

goroutine 1 [陈接收]:   main.main()           D:/Code/go/src/testconcurrency/main.go:23 + 0xca   退出状态2

避免此错误的有效方法是什么?,谢谢。

5 个答案:

答案 0 :(得分:6)

你有短暂的生产者&#34;他们只在通道上发送有限时间的值,你有一个无限的for循环从频道接收值无休止地,没有终止条件,并且通道仅在此无限循环之后关闭。一旦生产者停止发送价值,就会陷入僵局。

制作人必须关闭频道,表示不再发送任何值。由于你有多个生产者没有同步(生产者彼此不同步),一般你不能告诉哪一个会先完成,所以你不能指定一个关闭渠道(并且一个渠道可以只关闭一次,请参阅Why Go's channel can close twice?;以及Closing channel of unknown length)。

你必须&#34;协调&#34;生产者,当所有人完成工作时,协调员应关闭渠道。

消费者应该在频道上使用for range,因为for range构造会在关闭之前接收通道上发送的所有值,然后自动终止。

为了协调,建议使用sync.WaitGroup。无论您是在这种情况下使用全局的还是本地的,都将它传递给生产者,由您自己决定。使用本地将使解决方案更通用,更容易扩展。需要注意的一点是,您必须将指针传递给sync.WaitGroup。每当你启动一个新的生产者时,使用WaitGroup.Add()增加等待组。当生成器完成时,它可以使用WaitGroup.Done()发出信号,最好使用defer(因此无论如何都会运行,在异常情况下减轻死锁)。控制器可以等待所有生成器使用WaitGroup.Wait()完成。

这是一个完整的解决方案:

func producer(ch chan int, d time.Duration, num int, wg *sync.WaitGroup) {
    defer wg.Done()

    for i := 0; i < num; i++ {
        ch <- i
        time.Sleep(d)
    }
}

func main() {
    wg := &sync.WaitGroup{}
    ch := make(chan int)

    wg.Add(1)
    go producer(ch, 100*time.Millisecond, 2, wg)
    wg.Add(1)
    go producer(ch, 200*time.Millisecond, 5, wg)

    go func() {
        wg.Wait()
        close(ch)
    }()

    for v := range ch {
        fmt.Println(v)
    }
}

输出(在Go Playground上尝试):

0
0
1
1
2
3
4

请参阅相关问题:Prevent the main() function from terminating before goroutines finish in Golang

答案 1 :(得分:5)

可以使用两个等待组以优雅的方式解决此问题。通过关闭渠道ch,我们向消费者发出没有更多数据的信号。

解决方案适用于更多消费者。

package main

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

func producer(ch chan<- int, d time.Duration, num int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < num; i++ {
        ch <- i
        time.Sleep(d)
    }
}

func consumer(ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for x := range ch {
        fmt.Println(x)
    }
}

func main() {
    ch := make(chan int)
    producers := &sync.WaitGroup{}
    consumers := &sync.WaitGroup{}

    producers.Add(2)
    go producer(ch, 100*time.Millisecond, 2, producers)
    go producer(ch, 200*time.Millisecond, 5, producers)

    consumers.Add(1)
    go consumer(ch, consumers)

    producers.Wait()
    close(ch)
    consumers.Wait()
}

答案 2 :(得分:3)

问题是<-ch正在阻止,因此如果您没有向频道添加任何新值,它将永久阻止。一种方法是用开关选择替换它,该选择也是阻塞但允许在多个通道上收听。您还必须添加退出渠道。在您的示例中,只要退出通道收到两个值,我们就可以中断。 break语句需要一个标签,因为我们想从switch和for循环中退出。

https://play.golang.org/p/wGdCulZDnrx

另一种方法是拥有多个输入通道,并在完成发送后立即关闭它们。为此,每个goroutine都需要它自己的频道,否则我们将在第一个goroutine完成时退出。

第三个选项是创建一个合并函数,将多个通道合并为一个。这允许将通道的创建移动到生产者中,因此它们在一个位置被创建,填充和关闭。合并功能相对复杂,但它从业务逻辑代码中删除,可以单独理解和测试。然后主要代码简化为:

ch1 := producer(100*time.Millisecond, 2)
ch2 := producer(200*time.Millisecond, 5)

for i := range merge(ch1, ch2) {
    fmt.Println(i)
}

https://play.golang.org/p/2mv8ILhJPIB

merge func来自https://blog.golang.org/pipelines

答案 3 :(得分:1)

您需要同步goroutines中的所有异步进程。您的主线程和goroutine线程不是同步进程。你的主线程永远不会知道何时停止从goroutines调用通道。由于主线程在通道上循环,它总是调用通道中的值,当goroutines完成并且通道停止发送值时,主线程无法再从通道获取值,因此条件变为死锁。为避免这种情况,请使用sync.WaitGroup来同步异步进程。

以下是代码:

package main

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

func producer(ch chan int, d time.Duration, num int, wg *sync.WaitGroup) {
    for i:=0; i<num; i++ {
        ch <- i;
        time.Sleep(d);
    }
    defer wg.Done();
}

func main() {
    wg  := &sync.WaitGroup{}
    ch  := make(chan int);

    wg.Add(2);
    go producer(ch, 100*time.Millisecond, 2, wg);
    go producer(ch, 200*time.Millisecond, 5, wg);

    go func() {   
    wg.Wait()
    close(ch)
    }()

    // print the outputs
    for i:= range ch {
        fmt.Println(i);
    }
}

https://play.golang.org/p/euMTGTIs83g

希望它有所帮助。

由于我的解决方案与已经回答的解决方案看起来有点类似,因此在修改之前将其更改为我原来的答案,以适应OP问题。

以下是代码:

package main

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

// producer produce values tobe sent to consumer
func producer(ch chan int, d time.Duration, num int, wg *sync.WaitGroup) {
    defer wg.Done();
    for i:=0; i<num; i++ {
        ch <- i;
        time.Sleep(d);
    }
}

// consumer consume all values from producers
func consumer(ch chan int, out chan int, wg *sync.WaitGroup) {
    defer wg.Done();
    for i:= range ch {
        out <- i
    }
}

// synchronizer synchronize all goroutines to avoid deadlocks
func synchronizer(ch chan int, out chan int, wgp *sync.WaitGroup, wgc *sync.WaitGroup) {
    wgp.Wait()
    close(ch)
    wgc.Wait()
    close(out)
}

func main() {
    wgp  := &sync.WaitGroup{}
    wgc  := &sync.WaitGroup{}
    ch  := make(chan int);
    out := make(chan int);

    wgp.Add(2);
    go producer(ch, 100*time.Millisecond, 2, wgp);
    go producer(ch, 200*time.Millisecond, 5, wgp);

    wgc.Add(1);
    go consumer(ch, out, wgc)

    go synchronizer(ch, out, wgp, wgc)

    // print the outputs
    for i:= range out {
        fmt.Println(i);
    }
}

使用consumer goroutine fan-in来自多个goroutine的所有输入,并从consumer goroutine中读取所有值。

希望它有所帮助。

答案 4 :(得分:0)

更简单的答案 - 其中一个制片人需要关闭频道,而消费者可以只关注频道。

package main

import (
    "fmt"
    "time"
)

func producer(ch chan int, d time.Duration, num int, closer bool) {

    for i:=0; i<num; i++ {
        ch <- i
        time.Sleep(d)   
    }
    if closer {
        close(ch)
    }
}

func main() {
    ch := make(chan int)

    go producer(ch, 100*time.Millisecond, 2, false)
    go producer(ch, 200*time.Millisecond, 5, true)

    for i := range ch {
        fmt.Println(i)
    }

}

当然,除非您知道哪个制作人总是最后完成,否则您不希望在实际代码中执行此操作。在其他答案中,更好的设计是基于WaitGroup的模式。但这是此代码避免死锁的最简单方法。