“不要通过通道移动数据,而是通过通道移动数据的所有权”的含义

AFP*_*555 1 go

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

阅读了一些有关它的文章,我发现有人在这里对渠道进行基准测试。他基本上说通道可以传输 10 MB/s,这当然必须取决于他的硬件。然后他说了一些我没完全听懂的话:

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

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

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

Hym*_*sco 5

通过通道移动数据:

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
Run Code Online (Sandbox Code Playgroud)

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

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

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[:]
Run Code Online (Sandbox Code Playgroud)

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

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

// start messing with data
data[0] = 999
data[1] = 1234
...
Run Code Online (Sandbox Code Playgroud)

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

传递所有权的想法是,在您给出对某物的引用后,您也放弃了对该事物的所有权,并且不会使用它。只要我们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[:]
}
Run Code Online (Sandbox Code Playgroud)
c := make(chan []int)

// spawn some goroutines that read from this channel

// send some data
sendData(c)
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

  • 更好的是:“sendData”的通道参数可以是仅发送的。 (2认同)