如何解决 Go 通道死锁?

Posted

技术标签:

【中文标题】如何解决 Go 通道死锁?【英文标题】:How to resolve Go channel deadlock? 【发布时间】:2020-01-24 01:46:34 【问题描述】:

我正在学习 Go 编程语言,最近我遇到一个问题,我尝试了很多方法来运行我的代码,但我无法正确运行。我怎样才能改变我的程序来做到这一点?

package main

import (
    "fmt"
    "sync"
)

type Task struct 
    Id       int
    Callback chan int


func main() 
    var wg sync.WaitGroup
    subTask := make([]Task, 100)
    for i := 0; i < 100; i++ 
        go func(i int) 
            task := Task
                Id:       i,
                Callback: make(chan int, 1),
            
            task.Callback <- i
            subTask = append(subTask, task)
        (i)
    

    for _, v := range subTask 
        wg.Add(1)
        go func(v Task) 
            defer wg.Done()
            x := <-v.Callback
            fmt.Printf("%d ", x)
        (v)
    
    wg.Wait()

【问题讨论】:

【参考方案1】:

如果一个频道是 nil,&lt;-c 从 c 接收将永远阻塞。因此陷入僵局 通道可能为 nil 的原因是第一个 for 循环中的一个 goroutine 在执行 goroutine 接收时可能尚未执行。

因此,如果您假设第一个 for 循环中的所有 goroutine 在第二个 for 循环开始之前执行,您的代码就可以正常工作。

在睡眠中添加可以向您展示差异,但您实际上应该解决的是问题。

还有一个问题可能是 subTask := make([]Task, 100) 这个语句在 slice 中创建了 100 个空任务 obj,然后 append 增加了更多,所以长度最终增长到 200。

https://play.golang.org/p/4bZDJ2zvKdF

【讨论】:

【参考方案2】:

一个问题是您要附加到切片而不是更新现有的切片项。此外,您不需要缓冲通道。

func main() 
    subTask := make([]Task, 100)
    for i := range subTask 
        go func(i int) 
            subTask[i] = Taski, make(chan int)
            subTask[i].Callback <- i
        (i)
    

    var wg sync.WaitGroup
    wg.Add(len(subTask))
    for _, v := range subTask 
        go func(v Task) 
            defer wg.Done()
            fmt.Println(<-v.Callback)
        (v)
    
    wg.Wait()

【讨论】:

警告:正如 Cerise 所指出的,在我上面的代码中仍然存在使用切片的竞争条件。最好避免使用另一个 chan 在 go-routines 之间共享一个切片 - 请参阅我稍后的答案。【参考方案3】:

subTask 上存在数据竞争。任务初始化 goroutine 读取和写入变量 subTask 没有同步。

程序的目的是创建和初始化一个包含 100 个 Task 值的切片,但它创建了一个具有 100 个零值 Tasks 的切片,并附加了 100 个已初始化的 Tasks(忽略数据竞争问题刚刚提到)。

通过将任务分配给切片元素来解决这两个问题:

for i := 0; i < 100; i++ 
    go func(i int) 
        task := Task
            Id:       i,
            Callback: make(chan int, 1),
        
        task.Callback <- i
        subTask[i] = task
    (i)

subTask 元素存在数据竞争。不能保证任务初始化 goroutine 在主 goroutine 覆盖这些元素之前完成对元素的写入。通过使用等待组来协调初始化 goroutine 和主 goroutine 的完成来修复:

subTask := make([]Task, 100)
for i := 0; i < 100; i++ 
    wg.Add(1)
    go func(i int) 
        task := Task
            Id:       i,
            Callback: make(chan int, 1),
        
        task.Callback <- i
        subTask[i] = task
        wg.Done()
    (i)

wg.Wait()

Run the code on the playground.

race detector 报告上述两种数据竞争。

如果问题中的代码是实际代码,而不是用于提问的最小示例,则根本不需要 goroutine。

【讨论】:

【参考方案4】:

你可以考虑一个任务而不是一个任务片。

我认为这保留了您创建 100 个独立读写通道的最初想法。它还避免了数据竞争。

func main() 
    subTasks := make(chan Task)
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ 
        wg.Add(1)
        go func(i int) 
            defer wg.Done()
            task := Taski, make(chan int)
            subTasks <- task
            task.Callback <- i
        (i)
    

    go func() 
        wg.Wait()
        close(subTasks)
    ()

    for v := range subTasks 
        go func(v Task) 
            fmt.Println(<-v.Callback)
        (v)
    

Run in the playground

【讨论】:

以上是关于如何解决 Go 通道死锁?的主要内容,如果未能解决你的问题,请参考以下文章

如何在没有超时/死锁的情况下在PROMELA进程中发送和接收?

如何解决 go 和 grpc 中的 pub-sub 问题?

如何获取无缓冲通道中的元素数量

死锁问题+使用通道时增加goroutine的数量

通道如何使用通道查找素数问题?

去通道和死锁