Go lang关闭管道死锁

时间:2012-12-10 22:19:25

标签: concurrency go deadlock pipeline

我正在使用Go语言处理数据导入作业,我想将每个步骤编写为闭包,并使用通道进行通信,即每个步骤都是并发的。问题可以通过以下结构来定义。

  1. 从数据源获取小部件
    1. 将源1中的翻译添加到小部件
    2. 将源2中的翻译添加到小部件
    3. 将来自源1的定价添加到小部件
    4. WidgetRevisions 添加到小部件
      1. 将源1中的翻译添加到 WidgetRevisions
      2. 将源2中的翻译添加到 WidgetRevisions
  2. 出于这个问题的目的,我只处理必须在新的小部件上采取的前三个步骤。我假设在此基础上,第四步可以实现为一个管道步骤,它本身是用一个子三步管道实现来控制* WidgetRevision * s

    为此,我一直在编写一些代码来为我提供以下API:

    // A Pipeline is just a list of closures, and a smart 
    // function to set them all off, keeping channels of
    // communication between them.
    p, e, d := NewPipeline()
    
    // Add the three steps of the process
    p.Add(whizWidgets)
    p.Add(popWidgets)
    p.Add(bangWidgets)
    
    // Start putting things on the channel, kick off
    // the pipeline, and drain the output channel
    // (probably to disk, or a database somewhere)
    go emit(e)
    p.Execute()
    drain(d)
    

    我已经实现了它(代码位于GistGo Playground),但它已经死锁,100%成功失败率

    在调用p.Execute()时出现死锁,因为可能其中一个频道最终没有任何事情可做,任何一个都没有被发送,也没有工作要做......

    emit()drain()添加几行调试输出,我看到以下输出,我相信闭包调用之间的流水线是正确的,我看到一些小部件被省略。

    Emitting A Widget
    Input Will Be Emitted On 0x420fdc80
    Emitting A Widget
    Emitting A Widget
    Emitting A Widget
    Output Will Drain From 0x420fdcd0
    Pipeline reading from 0x420fdc80 writing to 0x420fdd20
    Pipeline reading from 0x420fdd20 writing to 0x420fddc0
    Pipeline reading from 0x420fddc0 writing to 0x42157000
    

    以下是我对此方法的一些了解:

    • 我相信这个设计“挨饿”一个或多个协程并不罕见,我相信这就是为什么这会导致死锁。
    • 我更喜欢管道首先将事情送入其中(API会实现Pipeline.Process(*Widget)
      • 如果我可以做到这一点,那么排水可能是一个“步骤”,它没有将任何东西传递给下一个功能,这可能是一个更清洁的API
    • 我知道我没有实现任何类型的梯级缓冲区,所以我完全有可能只是重载机器的可用内存
    • 我真的不相信这是好的Go风格......但它似乎利用了很多Go功能,但这并不是真正的好处
    • 由于WidgetRevisions也需要一个管道,我想让我的管道更通用,也许interface{}类型是解决方案,我不知道去得好以确定它是否是是否合理。
    • 我被建议考虑实施互斥锁来防范竞争条件,但我相信我会保存,因为每个闭包都会在Widget结构的一个特定单元上运行,但是我很乐意接受教育关于那个话题。

    总结:如何修复此代码,应该我修复此代码,如果你是一个比我更有经验的程序员,你会如何解决这个问题? “顺序工作单位”问题?

1 个答案:

答案 0 :(得分:2)

我只是觉得我不会建立远离频道的抽象。显式管道。

你可以很容易地为所有实际的管道操作创建一个函数,看起来像这样:

type StageMangler func(*Widget)

func stage(f StageMangler, chi <-chan *Widget, cho chan<- *Widget) {
    for widget := range chi {
                f(widget)
                cho <- widget
    }
    close(cho)
}

然后你可以传递func(w *Widget) { w.Whiz = true}或类似的舞台建设者。

此时您的add可能会收集这些内容以及他们的工作人员数量,因此特定阶段可以更轻松地拥有 n 工作人员。

我不确定这比直接拼接通道更容易,除非你在运行时构建这些管道。