解释:不要通过共享记忆来进行交流;通过沟通

时间:2016-04-03 21:28:28

标签: go memory channel goroutine

我想知道对这句名言的最实际解释是什么:

  

不要通过共享记忆来进行交流;通过沟通分享记忆。 (R. Pike)

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

  

通道上的发送在该通道的相应接收完成之前发生。 (Golang Spec)

还有专门的golang article解释报价。 Andrew G的关键贡献是working example

好。有时太多谈论....我已经从Memory Spec报价中得出,并且还通过查看工作示例:

  

goroutine1通过通道向goroutine2发送(任何内容)后,goroutine1完成的所有更改(内存中的任何位置)必须在goroutine2通过同一通道接收后才可见。 (Golang Lemma by Me:)

因此,我得出了着名引语的脚踏实地的解释:

  

要同步两个goroutine之间的内存访问,您不需要通过通道发送该内存。足够好的是从频道接收(甚至没有)。您将看到goroutine发送(到频道)时发送的任何更改(在任何地方)。 (当然,假设没有其他goroutine写入相同的内存。)更新(2)8-26-2017

我实际上有两个问题:

1)我的结论是否正确?

2)我的解释有帮助吗?

更新(1) 我假设无缓冲的频道。让我们首先限制自己,避免用太多的未知数来改造自己。

请注意,我们还要关注两个goroutines的简单用法,这两个goroutine通过单个通道和相关的记忆效应进行通信而不是最佳实践 - 这超出了本问题的范围。

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

7 个答案:

答案 0 :(得分:16)

这个着名的引语如果太过分了,可能会有点混乱。让我们将其分解为更基本的组件,并正确定义它们:

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. 这意味着确实有一部分内存,物理或虚拟,可以从多个线程修改,也可以从这些线程中读取。没有明确的所有权概念,所有线程都可以同样访问内存空间。
  3. 这完全不同。在Go中,可以像上面一样共享内存,并且数据竞争可以很容易地发生,所以这实际上意味着,修改goroutine中的变量,它是一个简单的值,如int或复杂的数据结构像地图一样,通过通道机制将值或指针发送到不同的goroutine来放弃所有权。理想情况下,没有共享空间,每个goroutine只能看到它拥有的内存部分。
  4. 这里的通信只是意味着一个通道(只是一个队列)允许一个goroutine从中读取,因此被通知新内存部分的所有权,而另一个发送它并接受失去所有权。这是一个简单的消息传递模式。
  5. 总之,引用的含义可以概括为:

      

    不要通过使用共享内存和复杂的,容易出错的同步原语来过度设置线程间通信,而是使用goroutine(绿色线程)之间的消息传递,以便可以在这些之间按顺序使用变量和数据。

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

答案 1 :(得分:5)

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

答案 2 :(得分:4)

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

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

答案 3 :(得分:3)

这有两句话;为了更全面地理解这些必须首先分开看,然后杵在一起,所以: share memory by communicating.意味着,不同的线程不应该通过遵守严格且容易出错的内存可见性和内存屏障等同步策略来相互通信。(请注意,它可以完成,但很快就会变得复杂和非常数据竞赛的错误)。因此,请避免遵守手动编程可见性构造,这些构造主要通过Java等编程语言中的适当同步来实现。

A send on a channel happens before the corresponding receive from that channel completes.表示如果一个线程对存储区域进行了任何更改(写入),它应该将相同的(存储区域)传递给对该存储区域感兴趣的线程;请注意,这已经将内存范围限制为仅两个线程。

现在结合golang内存模型阅读以上两段,其中说for(int row=0; row < maxNumber; row++){ for(int column=row; column < maxNumber; column++) { if(row == column) { matAuto[row][column] = elements[row]; } else { int newSum = matAuto[row][column-1] + elements[column]; // assign with mirror matAuto[row][column] = newSum; matAuto[column][row] = newSum; } } } 之前发生的关系确认第一个goroutine的写入对于接收内存参考的第二个goroutine是可见的频道的另一端!

答案 4 :(得分:2)

让我简单明了一点。

  

不要通过共享内存进行通信。

就像你正在使用线程进行通信一样,你必须使用变量或互斥锁来锁定内存,以便在通信完成之前不允许有人对其进行读写。

  

通过沟通分享记忆

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

答案 5 :(得分:1)

  

1)我的结论是否正确?

我想是的,如果这意味着我希望它能做到。使用“偶然”术语的规范中的语言的原因是它提供了一种明确的沟通形式来表达想法。

您的描述存在的问题是您实际上没有明确定义您的偶然事件顺序。我想虽然你暗示订单。如果你的意思是“goroutine a在goroutine a之前运行的goroutine a中的操作将在goroutine b后也可以看到goroutine b也观察到相同的同步点” - 即使在这里,“同步点”定义不明确 - 虽然我希望你理解它。正如规范所做的那样,这一点可以在偶然事件中定义。

  

2)我的解释有帮助吗?

也许,不熟悉该主题的人或正在努力理解偶然描述风格的人可能会发现您的描述更容易理解。但是,您的描述应用中存在一些限制和潜在的实际问题,如下所示:

  • 您没有严格定义您正在谈论的“发送”是同步点。如果您的意思是在无缓冲通道上发送,那么是,这将创建一个共享同步点,通过规范引入严格的偶然事件顺序。
  • 虽然如果上述情况属实,您已经描述了一个同步点,这只涉及原始建议点的一侧。最初的建议包括“所有权转移”的概念,这与创建同步点或偶然事件的关系较少,而更多地与依赖于潜在共享内存的代码的长期维护有关。这个概念是,不是保留对两个地方的某些内存段的访问,而是创建单独的共享同步点(如互斥锁),而是可以将对象的引用从一个所有者传递到另一个所有者。以这种方式设计软件可以防止在同步点之外进行意外修改,这在软件中经常可以通过大量使用互斥锁和大量使用共享内存来实现。

互斥锁或“显式同步点”与建议正好相反 - 它们是用于传达同步点“通过共享内存进行通信”的共享内存块,而即使它们具有深层次的互斥量引擎,通道是一种抽象机制,用于将对象的所有权(发送的值)从一个goroutine(发送者)传递给另一个goroutine(接收者)。有一点,忽略了如何实现通道,用户通过将其从一个所有者(goroutine a)传递到另一个(goroutine b)来共享内存(值)。如果您使用通道发送不相关的数据来创建同步点,那么您实际上将其用作互斥锁,并且通过共享内存(专注于通道)而不是通过通信共享来更接近通信(专注于价值)。

我希望这会有所帮助。

答案 6 :(得分:0)

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

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