如何理解golang内存模型中的通道通信规则?

8
在学习golang的过程中,当尝试理解内存模型规范中描述的通道通信时,我有些困惑,其中描述如下:
  1. 从通道发送数据在相应的接收操作完成之前发生。
  2. 关闭通道在因通道关闭而返回零值的接收操作之前发生。
  3. 从非缓冲通道接收数据在发送操作完成之前发生。
  4. 在容量为C的通道上第k个接收操作在第k+C个发送操作完成之前发生。
第1、2条规则很清晰易懂,而对于第3条规则,我真的感到困惑,因为它似乎与其他规则相反…… 我是否漏掉了什么关于非缓冲通道的特殊说明?或者如果按照规范中的示例将其理解为以下内容,我是正确的吗?
var c = make(chan int)
var a string

func f() {
    a = "hello, world"
    <-c    // A
}
func main() {
    go f()
    c <- 0 // B
    print(a)
}

对于非缓冲通道,发送操作(B)将被阻塞,直到接收者准备好接收值(A)为止?(比如:B开始执行并一直等待,直到A执行)这个描述准确吗?
我在Effective Go spec中发现了以下陈述,但与我的理解仍有差异...所以请问有人能用简单明了的方式来解释一下吗?
“接收者总是阻塞,直到有数据可接收。如果通道未缓存,则发送方将阻塞,直到接收方接收到该值。如果通道具有缓存,则发送方仅阻塞,直到该值已复制到缓存中;如果缓存已满,则这意味着等待某个接收方检索一个值。”

2
“Happens Before” HB 有着非常特殊的含义,与线性时间中传统的“before”只有松散的关系。你可以有两个事件 xy,同时满足这两个关系:x HB yy HB x。规则1和3共同大致表示:“在无缓冲通道上发送和接收会适当地同步并像您期望的那样工作(无论 CPU 如何重新组织您的指令,内存缓存如何缓存等等)。” - Volker
1
@Volker 嗯…我仍然困惑为什么 x HB yy HB x 都是正确的,哈哈。可能我需要将事件 e 分成不同的原子阶段,例如开始、处理和结束,以便更好地理解这个“happens-before”术语…谢谢! - Roy Ling
3
HB并不意味着某个事件实际上比另一个事件发生得更早,它只是一个方便的记忆方法。HB图通过部分HB序列决定了哪些内存操作在何处可见。它是一个“模型”,而不是现实。HB确立了同步的保证,但并不说明实际发生了什么。 - Volker
明显,“happens-before”这个术语是具有误导性的,可以看看这个例子https://play.golang.org/p/1NJrpo-hnjQ... 我发现自己在使用通道作为信号时对代码执行流程感到有些困惑。 - Victor
3个回答

16

您所标记的那句话是您正在寻找的简单解释。

如果通道未缓冲,则发送方会一直阻塞,直到接收方接收到该值为止。

这是另一种表述第三点的方式:

从未缓冲的频道接收发生在该频道上的发送完成之前。

当您在未缓冲的通道上发送时,发送方会被阻塞,直到接收方获取该值。这意味着接收发生在发送完成之前

缓冲通道是不同的,因为值有地方可以存放。如果您仍然感到困惑,可能举个例子会有帮助:

假设我想在您家里留下一个包裹:

  • 如果通道是缓冲的,则您有一个地方让我放置包裹 - 可能是邮箱。这意味着我可以完成任务(发送通道)而不必等待您接收它(检查您的邮箱)。
  • 如果通道没有缓冲,则我必须一直等到您来到前门并将包裹从我手中取走。您在我完成交付任务之前就会收到包裹。

对于未缓冲的通道,发送操作(B)被阻塞直到接收方准备好接收值(A)?(如:B开始并在A执行之前就不返回)这是准确的吗?

是的。 这是正确的。


2
非常好的例子,非常有帮助!谢谢! - Roy Ling
1
“发生在之前”与“执行在之前”的意义完全不同,非常好的解释。虽然我不明白这方面需要记住什么重要的事情,我的意思是,如果发送者先于接收者发生或接收者先于发送者发生,只要没有死锁,我应该注意什么重要的事情。我想看到代码示例,解释这两种情况(Timothy您的帖子非常好,但仍然需要考虑它如何影响我们的代码设计决策)。感谢大家。 - Victor
1
优秀的示例,易于理解!谢谢 - Jerry An
讲解得很清楚,谢谢。 - unifreak

2
对于非缓冲通道,发送操作(B)会被阻塞,直到接收方准备好接收值(A)为止(类似于:B开始并且不返回直到A执行)。这种说法是准确的。就像文档所说,如果一个通道是非缓冲的,那么发送者将会一直阻塞直到值被接收。如果值从未被接收,你将会遇到死锁,并且程序将会超时,就像在这个示例中一样。
var c = make(chan int)

func main() {
    c <- 0
    println("Will not print")
}

因此,非缓冲通道将在发送操作时阻塞,直到接收方准备好接收该值,即使这需要一段时间。然而,使用缓冲通道,阻塞将发生在接收操作上。 此示例 显示了非缓冲通道等待接收值,但缓冲通道不会:

package main

import "time"

var c chan int
var a string

func f() {
    time.Sleep(3)
    a = "hello, world"
    <-c // A
}
func test() {
    a = "goodbye"
    go f()
    c <- 0 // B
    println(a)
}

func main() {
    // Unbuffered
    c = make(chan int)
    test()

    // Buffered
    c = make(chan int, 1)
    test()
}

输出:

hello, world
goodbye

我认为你把 AB 搞反了。楼主的陈述是正确的——B 会一直阻塞,直到 A 发生(如果 A 没有发生则会永远阻塞)。 - Timothy Jones
@TimothyJones 是的,你说得对。我提供的例子反映了这一点,但我的陈述并没有。 - Jack Gore
@JackGore 很好的缓冲与非缓冲通道示例,现在很清楚了。还有有趣的超时示例。谢谢! - Roy Ling
@JackGore 我认为更好的称呼是“死锁”示例,而不是超时:程序因死锁而退出。此外,我发现这篇关于超时的博客很有趣,它提到了通过select在无缓冲通道上进行非阻塞发送的方法。 - Roy Ling

0

对我来说,当我读到下一个规则时,为什么必须这样做开始变得有意义:

在容量为C的通道上的第k个接收发生在该通道的第k+C个发送完成之前。

这有点相同,您需要能够同步例程,因此接收器等待发送器。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接