【问题标题】:Go lang closure pipeline deadlockGo lang 关闭管道死锁
【发布时间】:2012-12-10 22:19:24
【问题描述】:

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

  1. 从数据源获取Widgets
    1. 将源 1 中的翻译添加到 Widgets
    2. 将源 2 中的翻译添加到 Widgets
    3. 将来源 1 的定价添加到 小部件
    4. WidgetRevisions 添加到 Widgets
      1. 将源 1 中的翻译添加到 WidgetRevisions
      2. 将源 2 中的翻译添加到 WidgetRevisions

就这个问题而言,我只处理前三个步骤,这些步骤必须在新的 Widget 上执行。我假设在此基础上,第四步可以作为一个管道步骤来实现,它本身是根据一个子三步管道来实现的,以控制 *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{} 类型是解决方案,我不知道 Go 是否足够好以确定这是否明智或还没有。
  • 有人建议我考虑实施互斥锁以防止出现竞争条件,但我相信我会省心,因为每个闭包都会在 Widget 结构的一个特定单元上运行,但是我很乐意接受教育关于那个话题。

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

【问题讨论】:

  • 我已经解决了死锁问题,问题有两个,case i == len(pipe.processes) 错了,len() 总是+1(菜鸟错误)。
  • 我也将该条件实现为in, out = out, pipe.Drain,而不是制作一个新管道,这很愚蠢......但是关于这是一个体面的设计立场的问题。我觉得它有可能在我面前爆炸,并且类型不可重用,尽管我对流水线机制非常满意......
  • 最终的代码在play.golang.org/p/YXYLFR3qcz,但它迫切需要变得更通用。
  • 线索是,在每次运行中,Output Will Drain From 0x420fdcd0 行具有相同的值(包括地址),但该地址不会在管道 i/o 输出中重复。这让我查看了 Execute() 函数的 end case,在那里我看到了我的错误。
  • 顺便说一句,使用var x = func()... 有点像舞台表演。只需使用func x()

标签: concurrency go deadlock pipeline


【解决方案1】:

我只是不认为我会构建远离渠道的抽象。显式管道。

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

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}或者类似stage builder。

此时您的 add 可以收集这些及其工人数量,因此特定阶段可以更轻松地拥有 n 个工人。

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

【讨论】:

  • 如果您可以为此添加一些使用示例,那么我很乐意接受并投票赞成您的答案,因为函数定义更简单,而且它不依赖于单通道,这似乎更正确-per-object-per-stage plus-one as my implementation.
  • 每个阶段都有一个通道没有任何问题,每个通道都有一个 goroutine 来进行处理。我不知道确切我将如何实现你正在做的事情,因为我不知道确切你的需求是什么。但是拥有一个*Widget 输出通道,而不是从中读取,让另一个东西(或一组东西)从中读取并输出到管道中的另一个*Widget 输出通道对于像我这样的老UNIX程序员来说感觉很自然。
  • 我想问你你会实现上面的方法,看起来它还需要做很多通道处理。其实我想并行化一个顺序的工作流,这就是最终的目标,whiz、pop和bang都在真实的例子中向widget添加(远程查询)数据,顺序执行它们似乎很浪费。
  • 我已经实现了类似的东西。如果您希望它们同时发生,您需要一种方法来同时更新它们,否则这很简单。您确实需要在循环中将指针通过管道传递到每个通道,并且可能在小部件上有一个 WaitGroup 以了解它何时被完全处理。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-06-24
  • 2018-02-02
  • 1970-01-01
  • 2021-06-16
  • 2021-12-25
  • 2020-01-24
  • 1970-01-01
相关资源
最近更新 更多