说明:不要通过共享内存进行通信;通过通信共享内存
Posted
技术标签:
【中文标题】说明:不要通过共享内存进行通信;通过通信共享内存【英文标题】:Explain: Don't communicate by sharing memory; share memory by communicating 【发布时间】:2016-07-23 07:57:35 【问题描述】:我想知道对这句名言最接地气的解释是什么:
不要通过共享内存进行交流;通过通信共享内存。 (R.派克)
在The Go Memory Model我可以读到这个:
通道上的发送发生在该通道的相应接收完成之前。 (Golang 规范)
还有一个专门的golang article 解释报价。主要贡献是 working example 也是由 Andrew G 提供的。
嗯。有时谈论太多了....我从 Memory Spec 引文中得出,并且还通过查看工作示例得出:
在 goroutine1 通过通道向 goroutine2 发送(任何内容)后,goroutine1 所做的所有更改(内存中的任何位置)在通过同一通道接收后必须对 goroutine2 可见。 (我的 Golang 引理:)
因此,我得出这句名言的脚踏实地的解释:
要在两个 goroutine 之间同步内存访问,您不需要通过通道发送该内存。足够好是从频道接收(甚至什么都没有)。您将看到在发送时由 goroutine 发送(到通道)写入(任何地方)的任何更改。 (当然,假设没有其他 goroutine 正在写入相同的内存。)更新 (2) 8-26-2017
其实我有两个问题:
1) 我的结论正确吗?
2) 我的解释有帮助吗?
更新(1) 我假设 无缓冲频道。让我们先把自己限制在这一点上,以避免用太多的未知数来彻底检查自己。
请让我们也关注一个简单的用例,即两个 goroutine 通过单个通道进行通信以及相关的记忆效应,而不是最佳实践——这超出了这个问题的范围。
为了更好地理解我的问题的范围,假设 goroutine 可以访问任何类型的内存结构 - 不仅是原始的 - 它可以是一个大的,它可以是一个字符串、映射、数组等等。
【问题讨论】:
【参考方案1】:我不这么认为。要点不是用锁或其他并发原语来保护一个固定的内存地址,而是可以通过一种方式构建程序,即仅允许一个执行流访问该内存设计。
实现这一点的简单方法是通过通道共享对内存的引用。 一旦您通过频道发送参考,您忘记它。这样,只有将使用该通道的例程才能访问它。
【讨论】:
是的,要点是完全不同的东西——正如你在第二段中所描述的那样。但如果我错了,那我错在哪里呢?【参考方案2】:这句名言如果过于乱七八糟,可能会有点令人困惑。让我们将其分解为更基本的组件,并正确定义它们:
Don't communicate by sharing memory; share memory by communicating
---- 1 ---- ------ 2 ----- ---- 3 ----- ----- 4 -----
-
这意味着不同的执行线程将通过读取将在其他地方修改的内存来通知其他线程的状态变化。
一个完美的例子(尽管是针对进程而不是线程)是 POSIX 共享内存 API:http://man7.org/linux/man-pages/man7/shm_overview.7.html。
这种技术需要适当的同步,因为数据竞争很容易发生。
这意味着确实有一部分内存,无论是物理的还是虚拟的,都可以从多个线程中修改,也可以从这些线程中读取。没有明确的所有权概念,所有线程都可以平等地访问内存空间。
这是完全不同的。在 Go 中,像上面那样共享内存是可能的,并且很容易发生数据竞争,所以这实际上意味着,修改 goroutine 中的变量,无论是像
int
这样的简单值还是像 a 这样的复杂数据结构映射,并通过通道机制将值或指向该值的指针发送到不同的 goroutine 来放弃所有权。所以理想情况下,没有共享空间,每个 goroutine 只能看到它拥有的那部分内存。
这里的通信仅仅意味着一个通道,它只是一个队列,允许一个 goroutine 从中读取,因此被通知新部分内存的所有权,而另一个发送它并接受失去所有权。这是一种简单的消息传递模式。
总之,这句话的意思可以总结如下:
不要通过使用共享内存和复杂、容易出错的同步原语来过度设计线程间通信,而是在 goroutine(绿色线程)之间使用消息传递,以便变量和数据可以在它们之间按顺序使用。
这里使用的词序列值得注意,因为它描述了启发 goroutine 和通道概念的哲学:Communicating Sequential Processes。
【讨论】:
感谢您的贡献@SirDarius。到目前为止,我问的这个问题给我带来了很多乐趣:) 我希望其他人也一样。但是,您对我的问题的回答是 1) 否 2) 否? 我会说,是的,是的,即使我不确定你的结论的措辞对于 Go 开发人员来说是否“惯用”,如果你明白我的意思的话。缺少的本质上是所有权传递的概念。 您的回答中的这一部分确实具有误导性:“......并通过通道机制将值或指向该值的指针发送到不同的 goroutine 来放弃所有权” - 没有概念go 中的所有权,实际上假设如果发送 goroutine 再次修改数据,则会导致竞争条件。 由于实际上没有所有权的概念,这确实取决于最佳实践,您说您的问题与此无关。不过,我认为“通过交流共享内存”是最佳实践的指南,具体来说,不要重复使用您通过渠道发送的变量。【参考方案3】:本质上,是的。在通道发送之前分配给变量的任何值都有资格在通道读取之后被观察,因为通道操作施加了排序约束。但重要的是要记住等式的另一部分:如果要保证观察到这些值,则必须确保在写入和写入之间没有其他人可以写入这些变量。读。显然使用锁是可能的,但同时没有意义,因为如果你已经结合了锁和跨线程内存修改,你从通道中得到什么好处?您可以传递像布尔值这样简单的东西,作为允许独占访问全局数据的令牌,并且就内存模型保证而言(只要您的代码没有错误),它将是 100% 正确的,这可能是一个糟糕的设计,因为你会在没有充分理由的情况下让事情变得隐含和远距离行动;显式传递数据通常会更清晰且不易出错。
【讨论】:
到目前为止,这最能解决我的问题。谢谢。仍在等待 Rob P. 说出他的角色:)【参考方案4】:1) 我的结论正确吗?
我想是的,如果这意味着我希望它做的事。规范中的语言使用“偶然”术语的原因是它提供了一种明确定义的交流方式来表达想法。
您的描述的问题在于您实际上并没有明确定义您的偶然顺序。我认为虽然你是在暗示一个命令。如果您的意思是“在 goroutine a 对特定同步点进行操作之前发生的 goroutine a 中的操作将在 goroutine b 也观察到同一同步点之后对 goroutine b 可见” - 即使在这里,“同步点”定义不明确-尽管我希望您能理解。这样的点可以像规范那样偶然定义。
2) 我的解释有帮助吗?
也许,不熟悉该主题或难以理解描述的偶然风格的人可能会发现您的描述更容易解释。但是,您描述的应用程序存在局限性和潜在的实际问题,如下所示:
您没有严格定义您所说的“发送”是同步点。如果您的意思是在无缓冲通道上发送,那么是的,这将创建一个共享同步点,按照规范引入严格的偶然顺序。 虽然假设上述情况属实,但您已经描述了一个同步点,但这仅涉及原始建议点的一侧。最初的建议包括“所有权转移”的概念,这与创建同步点或偶然事件关系不大,而与依赖潜在共享内存的代码的长期维护有关。这个概念是,不是在两个地方保留对某个内存段的访问,并创建单独的共享同步点(如互斥锁),而是可以将对象的引用(可能是唯一的)从一个所有者传递给另一个所有者。以这种方式设计软件可以防止同步点之外的意外修改,这在使用细粒度的互斥锁和大量使用共享内存的软件中经常出现。互斥锁或“显式同步点”与建议完全相反——它们是一块共享内存,用于与“通过共享内存进行通信”的同步点进行通信,然而,即使它们在引擎盖,通道是一种抽象机制,用于将对象(发送的值)的所有权从一个 goroutine(发送者)传递到另一个 goroutine(接收者)。忽略通道是如何实现的,关键是用户通过将其从一个所有者(goroutine a)传递给另一个所有者(goroutine b)来共享内存(值)。如果使用通道发送不相关的数据来创建同步点,本质上就是将其用作互斥体,这更接近于通过共享内存进行通信(专注于通道),而不是通过通信进行共享(专注于值)。
我希望这会有所帮助。
【讨论】:
感谢您指出缓冲频道!我已经更新了我的问题,因此很明显我首先假设的是无缓冲频道。【参考方案5】:这里面有两句话;为了更全面地理解这些必须先分开看,然后结合在一起,所以:
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 可见频道的!
【讨论】:
【参考方案6】:让我在这里简单点。
不要通过共享内存进行交流。
这就像当您使用线程进行通信时,例如您必须使用变量或互斥锁来锁定内存,以便在通信完成之前不允许有人对其进行读写。
通过交流共享内存
在 go 例程中,值在通道上移动而不是阻塞内存,发送方通知接收方从该通道接收,因此它通过与接收方通信来共享内存以从通道获取。
【讨论】:
【参考方案7】:通过共享内存进行通信: 这是处理多线程的“传统”方式。如果某些数据由两个线程共享,例如为了防止它们尝试同时写入两个线程,则必须使用一些同步原语。众所周知,这很难调试。有趣的是,在时间共享中,处理器将时间片(例如 100 毫秒)分配给一个线程,然后切换上下文,并将下一个时间片分配给另一个线程。当操作系统不知道线程运行的代码在做什么时,就会发生切换。如果操作不是原子的,它们可以在任何步骤被操作系统中断。
通过通信共享内存: 通常情况下,使用一个参与者模型就是这种情况,其中每个参与者都映射到一个处理器内核。参与者使用邮箱系统交换消息,接收者在邮箱系统中接收消息的副本。没有共享数据,因此不需要像互斥锁这样的同步机制。这种架构的另一个名称是“无共享”。
【讨论】:
以上是关于说明:不要通过共享内存进行通信;通过通信共享内存的主要内容,如果未能解决你的问题,请参考以下文章