码农桃花源
  • README
  • channel
    • 06 - 从一个关闭的 channel 仍然能读出数据吗
    • 12 - channel 有哪些应用
    • 08 - 如何优雅地关闭 channel
    • 10 - channel 在什么情况下会引起资源泄漏
    • 00 - 什么是 CSP
    • channel 底层的数据结构是什么
    • 09 - channel 发送和接收元素的本质是什么
    • 11 - 关于 channel 的 happened-before 有哪些
    • 04 - 向 channel 发送数据的过程是怎样的
    • 03 - 从 channel 接收数据的过程是怎样的
    • 07 - 操作 channel 的情况总结
    • 05 - 关闭一个 channel 的过程是怎样的
  • map
    • map 的底层实现原理是什么
    • 可以边遍历边删除吗
    • map 的删除过程是怎样的
    • 可以对 map 的元素取地址吗
    • 如何比较两个 map 相等
    • 如何实现两种 get 操作
    • map 是线程安全的吗
    • map 的遍历过程是怎样的
    • map 中的 key 为什么是无序的
    • float 类型可以作为 map 的 key 吗
    • map 的赋值过程是怎样的
    • map 的扩容过程是怎样的
  • interface
    • iface 和 eface 的区别是什么
    • Go 接口与 C++ 接口有何异同
    • 如何用 interface 实现多态
    • 接口转换的原理
    • Go 语言与鸭子类型的关系
    • 值接收者和指针接收者的区别
    • 接口的构造过程是怎样的
    • 编译器自动检测类型是否实现接口
    • 类型转换和断言的区别
    • 接口的动态类型和动态值
  • 标准库
    • context
      • context.Value 的查找过程是怎样的
      • context 如何被取消
      • context 有什么作用
      • context 是什么
    • unsafe
      • 如何利用unsafe包修改私有成员
      • Go指针和unsafe.Pointer有什么区别
      • 如何实现字符串和byte切片的零拷贝转换
      • 如何利用unsafe获取slice&map的长度
  • goroutine 调度器
    • M 如何找工作
    • goroutine 调度时机有哪些
    • 什么是 go shceduler
    • goroutine 如何退出
    • 描述 scheduler 的初始化过程
    • 什么是workstealing
    • mian gorutine 如何创建
    • 什么是M:N模型
    • g0 栈何用户栈如何切换
    • schedule 循环如何运转
    • GPM 是什么
    • schedule 循环如何启动
    • sysmon 后台监控线程做了什么
    • goroutine和线程的区别
    • 一个调度相关的陷阱
  • 编译和链接
    • 逃逸分析是怎么进行的
    • GoRoot 和 GoPath 有什么用
    • Go 编译链接过程概述
    • Go 编译相关的命令详解
    • Go 程序启动过程是怎样的
  • 反射
    • Go 语言中反射有哪些应用
    • 什么情况下需要使用反射
    • 如何比较两个对象完全相同
    • Go 语言如何实现反射
    • 什么是反射
  • 数组和切片
    • 数组和切片有什么异同
    • 切片作为函数参数
    • 切片的容量是怎样增长的
  • GC
    • GC
Powered by GitBook
On this page

Was this helpful?

  1. channel

11 - 关于 channel 的 happened-before 有哪些

维基百科上给的定义:

In computer science, the happened-before relation (denoted: ->) is a relation between the result of two events, such that if one event should happen before another event, the result must reflect that, even if those events are in reality executed out of order (usually to optimize program flow).

简单来说就是如果事件 a 和事件 b 存在 happened-before 关系,即 a -> b,那么 a,b 完成后的结果一定要体现这种关系。由于现代编译器、CPU 会做各种优化,包括编译器重排、内存重排等等,在并发代码里,happened-before 限制就非常重要了。

根据晃岳攀老师在 Gopher China 2019 上的并发编程分享,关于 channel 的发送(send)、发送完成(send finished)、接收(receive)、接收完成(receive finished)的 happened-before 关系如下:

  1. 第 n 个 send 一定 happened before 第 n 个 receive finished,无论是缓冲型还是非缓冲型的 channel。

  2. 对于容量为 m 的缓冲型 channel,第 n 个 receive 一定 happened before 第 n+m 个 send finished。

  3. 对于非缓冲型的 channel,第 n 个 receive 一定 happened before 第 n 个 send finished。

  4. channel close 一定 happened before receiver 得到通知。

我们来逐条解释一下。

第一条,我们从源码的角度看也是对的,send 不一定是 happened before receive,因为有时候是先 receive,然后 goroutine 被挂起,之后被 sender 唤醒,send happened after receive。但不管怎样,要想完成接收,一定是要先有发送。

第二条,缓冲型的 channel,当第 n+m 个 send 发生后,有下面两种情况:

若第 n 个 receive 没发生。这时,channel 被填满了,send 就会被阻塞。那当第 n 个 receive 发生时,sender goroutine 会被唤醒,之后再继续发送过程。这样,第 n 个 receive 一定 happened before 第 n+m 个 send finished。

若第 n 个 receive 已经发生过了,这直接就符合了要求。

第三条,也是比较好理解的。第 n 个 send 如果被阻塞,sender goroutine 挂起,第 n 个 receive 这时到来,先于第 n 个 send finished。如果第 n 个 send 未被阻塞,说明第 n 个 receive 早就在那等着了,它不仅 happened before send finished,它还 happened before send。

第四条,回忆一下源码,先设置完 closed = 1,再唤醒等待的 receiver,并将零值拷贝给 receiver。

参考资料【鸟窝 并发编程分享】这篇博文的评论区有 PPT 的下载链接,这是晁老师在 Gopher 2019 大会上的演讲。

关于 happened before,这里再介绍一个柴大和曹大的新书《Go 语言高级编程》里面提到的一个例子。

书中 1.5 节先讲了顺序一致性的内存模型,这是并发编程的基础。

我们直接来看例子:

var done = make(chan bool)
var msg string

func aGoroutine() {
    msg = "hello, world"
    done <- true
}

func main() {
    go aGoroutine()
    <-done
    println(msg)
}

先定义了一个 done channel 和一个待打印的字符串。在 main 函数里,启动一个 goroutine,等待从 done 里接收到一个值后,执行打印 msg 的操作。如果 main 函数中没有 <-done 这行代码,打印出来的 msg 为空,因为 aGoroutine 来不及被调度,还来不及给 msg 赋值,主程序就会退出。而在 Go 语言里,主协程退出时不会等待其他协程。

加了 <-done 这行代码后,就会阻塞在此。等 aGoroutine 里向 done 发送了一个值之后,才会被唤醒,继续执行打印 msg 的操作。而这在之前,msg 已经被赋值过了,所以会打印出 hello, world。

这里依赖的 happened before 就是前面讲的第一条。第一个 send 一定 happened before 第一个 receive finished,即 done <- true 先于 <-done 发生,这意味着 main 函数里执行完 <-done 后接着执行 println(msg) 这一行代码时,msg 已经被赋过值了,所以会打印出想要的结果。

进一步利用前面提到的第 3 条 happened before 规则,修改一下代码:

var done = make(chan bool)
var msg string

func aGoroutine() {
    msg = "hello, world"
    <-done
}

func main() {
    go aGoroutine()
    done <- true
    println(msg)
}

同样可以得到相同的结果,为什么?根据第三条规则,对于非缓冲型的 channel,第一个 receive 一定 happened before 第一个 send finished。也就是说, 在 done <- true 完成之前,<-done 就已经发生了,也就意味着 msg 已经被赋上值了,最终也会打印出 hello, world。

Previous09 - channel 发送和接收元素的本质是什么Next04 - 向 channel 发送数据的过程是怎样的

Last updated 5 years ago

Was this helpful?