【问题标题】:Meaning of "don't move data over channels, move ownership of data over channels"“不要通过渠道移动数据,通过渠道移动数据所有权”的含义
【发布时间】:2021-08-29 22:26:59
【问题描述】:

我了解到 Golang 频道实际上比该语言提供的许多替代方案要慢。当然,它们确实很容易掌握,但是因为它们是高级结构,所以会带来一些开销。

阅读一些关于它的文章,我发现有人对频道进行基准测试here。他基本上说通道可以传输10 MB / s,这当然必须依赖于他的硬件。然后他说了一些我还没有完全理解的话:

如果您只想使用通道快速移动数据,那么移动它 1 一次一个字节是不明智的。您对频道的真正作用是 移动数据的所有权,在这种情况下,数据速率可以 实际上是无限的,取决于你的数据块的大小 转移。

我在多个地方都看到过这种“移动数据所有权”,但我还没有看到一个可靠的例子来说明如何做到这一点,而不是移动数据本身。

我想看一个例子来了解这个最佳实践。

【问题讨论】:

  • 通道避免了数据竞争(对相同数据的并发读取和写入,这可能导致不可预测和/或格式错误的读取/写入)。通道读取或写入的约定——即使读取/写入来自不同的 goroutine——读取/写入是原子的,因此避免了数据竞争。
  • "我了解到 Golang 通道实际上比该语言提供的许多替代方案要慢。当然,它们确实很容易掌握,但是由于它们是高级结构,因此会带来一些开销。”这是一个严重的过度简化(即使是真的)完全无趣,因为您的问题将是编写正确代码。痴迷于“速度”是不健康的。尤其是考虑到渠道足够快。如果没有基准来证明您的渠道是应用程序瓶颈,考虑渠道速度是浪费时间。
  • @Volker 但是为什么问这个有问题?这是一个完全有效的问题。如果有人已经确定渠道是他们的瓶颈怎么办?他们会发现这个问题很有用。已经有有趣的答案了。
  • “如果有人已经确定渠道是他们的瓶颈怎么办?”这与发现硬件 CPU 错误一样可能。可能但不太可能。问没有错,只是可能是在错误的时间问错了问题。学习一门语言时,过度关注“速度”或“性能”会导致糟糕的代码、坏习惯和损坏的软件。把软件弄好真是太难了。速度应该是第二个关注点,至少在学习期间是这样。

标签: go


【解决方案1】:

Hymns For Disco's answer 很好,但我发现写得好简短有时会回答一个有趣的挑战,我想我有一个类比在这里会有所帮助。

将您的数据想象成占据仓库,每个仓库的大小都相当于一个大街区。你有一千个仓库,分布在许多国家和城市。

你有五位高技能的技术人员,每个人都可以很好地完成一件事。您需要所有技术人员对所有数据进行操作。不幸的是,每个技术人员都讨厌其他四个,如果仓库中有任何一个,他们都不会做任何工作(甚至可能试图杀死其他人)。

解决这个问题的一种方法是建造五个额外的仓库,并将五名技术人员中的每一个放在新的五个仓库中。然后,您可以将 1000 个仓库中每个仓库的全部内容运送到各个备件,一次一个,然后在每个技术人员完成后将内容移回;并且您可以通过将内容移动到工作仓库#1、然后到#2、然后到#3 等等来稍微优化仓库内容移动,并且只有在它准备好离开后才将它移回原来的仓库#5。但显然,这需要大量的运输和物流,并且需要大量的时间和金钱来进行所有这些大宗运输。即使每个技术人员可以在一天之内完全处理整个仓库,也需要数年时间和大量资金。

或者,您可以派遣五名技术人员。将它们发送到仓库 (WH) 1-5。当 tech#1 完成 WH#1 后,将他移至 WH#2,除非 tech#2 还在;如果那是下一个免费的,请将他移至 WH#6。

我们移动的是轻巧的“工作人员”,而不是大而重的“占据人员工作空间的东西”。总体成本要低得多。不过,我们必须注意不要让技术人员意外相遇。

还要注意,如果数据本身又小又轻且易于移动,这种奇特的解决方案(即注意谁在什么时间可以访问哪些数据)并没有帮助。在小数据情况下,我们不妨移动数据,而不是工作人员。

【讨论】:

  • 扩展类比:移动资源很容易正确执行,但可能很昂贵。调动技术人员很便宜,但要正确完成并不容易。
【解决方案2】:

在通道上移动数据:

c := make(chan [1000]int)

// spawn some goroutines that read from this channel

var data [1000]int
// populate the data

// write data to the channel
c <- data

正如您所提到的,这里的潜在问题是您正在移动大量数据,因此您可能会进行过多的内存复制。

您可以通过在通道上发送引用类型(例如指针或切片)来防止这种情况发生:

c := make(chan []int)

// spawn some goroutines that read from this channel

var data [1000]int
// populate the data

// write a reference to data to the channel
c <- data[:]

所以我们只是进行了完全相同的数据传输,但减少了内存复制,对吗?好吧,这是一个潜在的问题:您通过通道发送了对 data 的引用,但 data 值在当前范围内仍然可以访问,即使在发送之后:

// write a reference to data to the channel
c <- data[:]

// start messing with data
data[0] = 999
data[1] = 1234
...

此代码可能刚刚引入了潜在的数据竞争,因为从通道读取该切片的人可能在您开始修改它的同时对其进行处理。

传递所有权的想法是,在您提供对某物的引用之后,您也放弃了该事物的所有权,并且不会使用它。只要我们在给出引用(在通道上发送切片)后不使用data,那么我们就已经正确地传递了所有权。


这个问题是共享状态的一般问题的扩展。与 Rust 不同,例如,Go 没有语言结构来正确控制共享状态。为了减少这些错误的机会,您可以应用一些策略:

  • 避免在通道上传递引用:在上面的示例中,一旦我们开始使用切片通过引用传递数据,就会出现问题。这样做只是为了减少完成的内存处理量。除非有实际的理由进行这种优化(测量了有价值的性能差异),否则它可以完全避免。尽管如此,Go 中仍有一些数据类型固有是引用(例如,地图和切片)。如果必须在通道上传递这些类型,则可以使用其他策略。
  • 将数据创建逻辑分解为函数:在上面的示例中,我们可以重构代码:
func sendData(c chan []int) {
    var data [1000]int
    // populate the data

    // write a reference to data to the channel
    c <- data[:]
}
c := make(chan []int)

// spawn some goroutines that read from this channel

// send some data
sendData(c)

错误使用data 的可能性仍然存在,但现在它被隔离为一个意图明确的小函数。理论上,隔离应该使代码更容易理解,更清楚data的正确用法是什么,并且更少的更改会与它有潜在的交互。

  • 不要将数据管道与持久状态混合在一起:数据管道是指两个或多个并发例程,数据通过通道在它们之间流动。扩展上一点,使自有引用的创建尽可能接近它们进入数据管道的位置。在 goroutine 接收数据的位置和再次发送或使用数据的位置之间留出空间,尽可能紧凑。在所有权的一般规则中,您只有在您目前拥有某物的完全所有权时才能转让其所有权。由于此规则,您应尽可能避免在通道上发送任何引用,而不仅仅是在在发送之前立即创建引用数据。如果您引用了任何持久或全局状态,则确保所有权得到尊重会变得更加困难。

通过将引用的创建和所有权转移保持在一个孤立的全局函数中,应该更难出错。那么违反所有权规则的唯一方法是:

  1. 泄露对全局状态的引用
  • 尽量消除全局变量和全局状态
  1. 泄露对引用类型参数状态的引用
  • 不要在数据发送函数中使用任何引用类型参数
  1. 发送参考后修改参考数据
  • 将发送操作放在函数的最后。如有必要,您可以将发送放入延迟调用中。

没有完美的解决方案可以消除所有共享状态问题(即使在 Rust 中它们有时在实践中也存在),但我希望这些策略能帮助您思考如何解决这个问题。

【讨论】:

  • 那么,有没有办法从当前上下文中真正删除指针的所有权?这似乎很脆弱,因为它似乎是知识问题。另一个开发人员可能不知道我放弃了该指针的所有权,除了语义之外,没有办法知道该指针是否会被 goroutine 修改。我喜欢 Go 并发,但这似乎是一个非常糟糕的问题。
  • @AFP_555 我认为你是对的。这是一个大问题。我已经更新了答案以对此进行扩展。
  • 更好的是:sendData 的通道参数可以只发送。
  • 嘿,我真的很喜欢你的回答。如果您不介意查看stackoverflow.com/questions/67979304/…,我创建了另一个关于并发的问题
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-18
  • 2017-11-09
  • 1970-01-01
  • 1970-01-01
  • 2021-12-03
  • 1970-01-01
相关资源
最近更新 更多