解释:不要通过共享内存来交流;通过通信共享内存

Explain: Don't communicate by sharing memory; share memory by communicating

不知对这句名言最接地气的解释是什么:

Don't communicate by sharing memory; share memory by communicating. (R. Pike)

The Go Memory Model 我可以读到这个:

A send on a channel happens before the corresponding receive from that channel completes. (Golang Spec)

Andrew G 也有专门的 golang article explaining the quote. And key contribution is a working example

嗯。有时谈论太多......我从 Memory Spec quotation 和查看工作示例中得出:

After a goroutine1 sends (anything) to a goroutine2 via a channel, then all changes (anywhere in the memory) done by goroutine1 must be visible to goroutine2 after it received via same channel. (Golang Lemma by Me:)

因此我得出了名言的脚踏实地的解释:

To synchronize memory access between two goroutines, you don't need to send that memory via channel. Good enough is to receive from the channel (even nothing). You will see any changes that were written (anywhere) by the goroutine sending (to the channel) at the time of it's send. (Of course, assuming no other goroutines are writing to the same memory.) Update (2) 8-26-2017

其实我有两个问题:

1)我的结论正确吗?

2) 我的解释有帮助吗?

更新(1) 我假设 无缓冲通道 。让我们首先将自己限制在这一点上,以避免用太多未知数来彻底检查自己。

请让我们也关注一个简​​单的用例,即两个 goroutines 通过单个通道进行通信以及相关的内存效果,而不是最佳实践 - 这超出了这个问题的范围。

为了更好地理解我的问题的范围,假设 goroutine 可以访问任何类型的内存结构——不仅是原始内存结构——而且它可以是一个很大的内存结构,它可以是一个字符串、映射、数组等等。

我不这么认为。要点不是用锁或其他并发原语保护一个固定的内存地址,而是可以以一种只允许一个执行流访问该内存的方式构建程序设计 .

实现这一点的简单方法是通过通道共享对内存的引用。 通过频道发送参考后,您 忘记它 。这样,只有使用该通道的例程才能访问它。

如果过于随意地理解这句名言,可能会有点令人困惑。让我们将其分解为更基本的组件,并正确定义它们:

Don't communicate by sharing memory; share memory by communicating
      ---- 1 ----    ------ 2 -----  ---- 3 -----    ----- 4 -----
  1. 这意味着不同的执行线程将通过读取将在其他地方修改的内存来获知其他线程的状态更改。 POSIX 共享内存 API: http://man7.org/linux/man-pages/man7/shm_overview.7.html 就是一个完美的例子(虽然是针对进程而不是线程)。 此技术需要适当的同步,因为数据竞争很容易发生。
  2. 这意味着确实有一部分内存,物理的或虚拟的,可以从多个线程修改,也可以从中读取。没有明确的所有权概念,所有线程都可以平等地访问内存 space。
  3. 这是完全不同的。在 Go 中,像上面那样共享内存是可能的,并且数据竞争很容易发生,所以这实际上意味着修改 goroutine 中的变量,无论是像 int 这样的简单值还是复杂的数据结构像地图一样,通过通道机制将值或指向值的指针发送到不同的 goroutine 来放弃所有权。所以理想情况下,没有共享 space,每个 goroutine 只能看到它拥有的那部分内存。
  4. 这里的通信仅仅意味着一个通道,它只是一个队列,允许一个 goroutine 从中读取并因此被告知新内存部分的所有权,而另一个 goroutine 发送它并接受失去所有权.这是一个简单的消息传递模式。

总而言之,这句话的意思可以总结如下:

Don't overengineer inter-thread communication by using shared memory and complicated, error-prone synchronisation primitives, but instead use message-passing between goroutines (green threads) so variables and data can be used in sequence between those.

这里使用的单词序列是值得注意的,因为它描述了启发 goroutines 和通道概念的哲学:Communicating Sequential Processes.

本质上,是的。在通道发送之前分配给变量的任何值都可以在通道读取之后被观察到,因为通道操作强加了顺序约束。但重要的是要记住等式的另一部分:如果你想保证观察到这些值,你必须确保没有其他人可以写入这些变量之间的变量写和读。显然使用锁是可能的,但同时没有意义,因为如果您已经结合了锁和跨线程内存修改,那么您从通道中获得什么好处?您可以传递像布尔值一样简单的东西,作为允许独占访问全局数据的令牌,并且在内存模型保证方面它是 100% 正确的(只要您的代码没有错误),这可能是一个糟糕的设计,因为你会在没有充分理由的情况下让事情变得隐含和远距离行动;显式传递数据通常会更清晰,更不容易出错。

1) Is my conclusion correct?

我想是的,如果这意味着我希望它做的那样。规范中的语言使用 "happenstance" 术语的原因是它为表达想法提供了定义明确的交流形式。

您的描述存在问题,您实际上并未明确定义您的偶然顺序。我认为你是在暗示命令。如果您的意思是 "operations in goroutine a that happen before goroutine a operates on a particular point of synchronization will be visible by goroutine b after goroutine b has also observed that same point of synchronization" - 即使在这里,"point of synchronization" 的定义也不明确 - 尽管我希望您理解它。这样的点可以像规范那样偶然定义。

2) Does my explanation help?

也许,不熟悉该主题或难以理解描述的偶然风格的人可能会发现您的描述更容易理解。但是您描述的应用存在局限性和潜在的实际问题,如下:

  • 你没有严格定义你所说的"send"是一个同步点。如果您的意思是在无缓冲通道上发送,那么是的,这将创建一个共享同步点,根据规范引入严格的偶然顺序。
  • 虽然假设上述情况成立,但您已经描述了一个同步点,这仅解决了原始建议点的一侧。最初的建议包括 "transfer of ownership" 的概念,这与创建同步点或偶然事件关系不大,而更多地与依赖于潜在共享内存的代码的长期维护有关。这个概念是,不是在两个地方保留对某些内存段的访问,而是创建单独的共享同步点(如互斥锁),而是可以将对象的引用从一个所有者传递给另一个所有者,这可能是唯一的。以这种方式设计软件可以防止同步点之外的意外修改,这种情况经常在使用互斥锁和广泛使用共享内存的软件中观察到。

互斥量或 "explicit synchronization points" 与建议完全相反 - 它们是共享内存,用于通信同步点 "communicate by sharing memory",然而,即使它们在底层有互斥量实际上,通道是一种抽象机制,用于将对象的所有权(发送的值)从一个 goroutine(发送者)传递给另一个 goroutine(接收者)。重点是,忽略通道的实现方式,用户通过将内存从一个所有者(goroutine a)传递给另一个所有者(goroutine b)来共享内存(值)。如果你正在使用一个通道来发送不相关的数据来创建一个同步点,那么你本质上是在将它用作一个互斥量,而这更接近于通过共享内存进行通信(重点是通道),而不是通过通信共享(重点是值)。

希望对您有所帮助。

这里面有两句话;为了更全面地理解这些必须首先分开看,然后组合在一起,所以: Don't communicate by sharing memory; 意味着,不同的线程不应该通过遵守严格且容易出错的内存可见性和同步策略(如内存屏障等)相互通信。(请注意,它可以做到,但它很快就会变得复杂且非常数据竞争的越野车)。因此,请避免遵守手动的、程序化的可见性构造,这些构造主要是通过 Java.

等编程语言中的适当同步实现的

share memory by communicating. 意味着如果一个线程对内存区域进行了任何更改(写入),它应该将相同的(内存区域)传递给对同一内存区域感兴趣的线程;请注意,这已经将内存范围限制为只有两个线程。

现在结合 golang 内存模型阅读以上两段,A send on a channel happens before the corresponding receive from that channel completes. happens-before 关系确认第一个 goroutine 的写入将对接收内存引用的第二个 goroutine 可见在通道的另一端!

让我简单点,开门见山。

Don't communicate by sharing memory.

这就像当您使用线程进行通信时,例如您必须使用变量或互斥锁来锁定内存,以便在通信完成之前不允许其他人对其进行读写。

Share memory by communicating

在 go 例程中,值在通道上移动而不是阻塞内存,发送方通知接收方从该通道接收,因此它通过与接收方通信以从通道获取来共享内存。

  1. 通过共享内存进行交流: 这是处理多线程的“传统”方式。如果某些数据由两个线程共享,例如为了防止它们同时写入两个线程,则必须使用一些同步原语。这是出了名的难以调试。有趣的是,在时间共享中,处理器正在为一个线程分配一个时间片(例如 100 毫秒),然后切换上下文,并将下一个时间片分配给另一个线程。切换发生时操作系统不知道线程的代码 运行 在做什么。如果操作不是原子操作,它们可以在任何步骤被操作系统中断。

  2. 通过通信共享内存: 通常,这是使用角色模型的情况,其中每个角色都映射到一个处理器内核。参与者使用邮箱系统交换消息,接收者在该系统中接收消息的副本。没有共享数据,因此不需要像互斥锁这样的同步机制。此架构的另一个名称是“无共享”。