go语言共享内存通信 go 内存共享

goroutine是不是内存共享goroutine和channel是Go语言非常棒的特色,它们提供了一种非常轻便易用的并发能力 。但是当您的应用进程中有很多goroutine的时候,如何在主流程中等待所有的goroutine 退出呢?
1 通过Channel传递退出信号
Go的一大设计哲学就是:通过Channel共享数据,而不是通过共享内存共享数据 。主流程可以通过channel向任何goroutine发送停止信号,就像下面这样:
func run(done chan int) {
for {
select {
case -done:
fmt.Println("exiting...")
done - 1
break
default:
}
time.Sleep(time.Second * 1)
fmt.Println("do something")
}
}
Golang 语言深入理解:channel 本文是对 Gopher 2017 中一个非常好的 Talk?: [Understanding Channel](GopherCon 2017: Kavya Joshi - Understanding Channels) 的学习笔记,希望能够通过对 channel 的关键特性的理解,进一步掌握其用法细节以及 Golang 语言设计哲学的管窥蠡测 。
channel是可以让一个 goroutine 发送特定值到另一个 gouroutine 的通信机制 。
原生的 channel 是没有缓存的(unbuffered channel),可以用于 goroutine 之间实现同步 。
关闭后不能再写入,可以读取直到 channel 中再没有数据,并返回元素类型的零值 。
gopl/ch3/netcat3
首先从 channel 是怎么被创建的开始:
在 heap 上分配一个 hchan 类型的对象,并将其初始化,然后返回一个指向这个 hchan 对象的指针 。
理解了 channel 的数据结构实现,现在转到 channel 的两个最基本方法:sends和receivces,看一下以上的特性是如何体现在sends和receives中的:
假设发送方先启动,执行 ch - task0 :
如此为 channel 带来了goroutine-safe 的特性 。
在这样的模型里 ,  sender goroutine - channel - receiver goroutine之间,hchan 是唯一的共享内存,而这个唯一的共享内存又通过 mutex 来确保 goroutine-safe ,所有在队列中的内容都只是副本 。
这便是著名的 golang 并发原则的体现:
发送方 goroutine 会阻塞 , 暂停,并在收到 receive 后才恢复 。
goroutine 是一种 用户态线程 , 由 Go runtime 创建并管理,而不是操作系统,比起操作系统线程来说 , goroutine更加轻量 。
Go runtime scheduler 负责将 goroutine 调度到操作系统线程上 。
runtime scheduler 怎么将 goroutine 调度到操作系统线程上?
当阻塞发生时,一次 goroutine 上下文切换的全过程:
然而,被阻塞的 goroutine 怎么恢复过来?
阻塞发生时,调用 runtime sheduler 执行 gopark 之前,G1 会创建一个 sudog ,并将它存放在 hchan 的 sendq 中 。sudog 中便记录了即将被阻塞的 goroutineG1 ,以及它要发送的数据元素 task4 等等 。
接收方 将通过这个 sudog 来恢复 G1
接收方 G2 接收数据, 并发出一个 receivce ,将 G1 置为runnable :
同样的, 接收方 G2 会被阻塞,G2 会创建 sudoq ,存放在 recvq ,基本过程和发送方阻塞一样 。
不同的是,发送方 G1如何恢复接收方 G2,这是一个非常神奇的实现 。
理论上可以将 task 入队 , 然后恢复 G2, 但恢复 G2后 , G2会做什么呢?
G2会将队列中的 task 复制出来 , 放到自己的 memory 中,基于这个思路,G1在这个时候,直接将 task 写到 G2的 stack memory 中!
这是违反常规的操作,理论上 goroutine 之间的 stack 是相互独立的 , 只有在运行时可以执行这样的操作 。
这么做纯粹是出于性能优化的考虑,原来的步骤是:
优化后,相当于减少了 G2 获取锁并且执行 memcopy 的性能消耗 。
channel 设计背后的思想可以理解为 simplicity 和 performance 之间权衡抉择,具体如下:

推荐阅读