

[译] Go语言的有缓冲channel和无缓冲channel
source link: https://www.tuicool.com/articles/QjIf2un
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go中的channel十分强大,理解channel的内部机制后再去使用它可以发挥出更大威力。另外,选择使用有缓冲channel还是无缓冲channel会影响到我们程序的行为表现,以及性能。
无缓冲channel
无缓冲channel在消息发送时需要接收者就绪。声明无缓冲channel的方式是不指定缓冲大小。以下是一个列子:
package main import ( "sync" "time" ) func main() { c := make(chan string) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() c <- `foo` }() go func() { defer wg.Done() time.Sleep(time.Second * 1) println(`Message: `+ <-c) }() wg.Wait() }
第一个协程会在发送消息 foo
时阻塞,原因是接收者还没有就绪:这个特性在 标准文档 中描述如下:
如果缓冲大小设置为0或者不设置,channel为无缓冲类型,通信成功的前提是发送者和接收者都处于就绪状态。
effective Go 文档也有相应的描述:
无缓冲channel,发送者会阻塞直到接收者接收了发送的值。
为了更好的理解channel的特性,接下来我们分析channel的内部结构。
内部结构
channel的结构体 hchan
被定义在 runtime
包中的 chan.go
文件中。以下是无缓冲channel的内部结构(本小节先介绍无缓冲channel,所以暂时忽略了 hchan
结构体中和缓冲相关的属性):
channel中持有两个链表,接收者链表 recvq
和发送者链表 sendq
,它们的类型是 waitq
。链表中的元素为 sudog
结构体类型,它包含了发送者或接收者的协程相关的信息。通过这些信息,Go可以在发送者不存在时阻塞住接收者,反之亦然。
以下是我们前一个例子的流程:
- 创建一个发送者列表和接收者列表都为空的channel。
- 第一个协程向channel发送
foo
变量的值,第16行。 - channel从池中获取一个
sudog
结构体变量,用于表示发送者。sudog结构体会保持对发送者所在协程的引用,以及foo
的引用。 - 发送者加入
sendq
队列。 - 发送者协程进入等待状态。
- 第二个协程将从channel中读取一个消息,第23行。
- channel将
sendq
列表中等待状态的发送者出队列。 - chanel使用
memmove
函数将发送者要发送的值进行拷贝,包装入sudog
结构体,再传递给channel接收者的接收变量。 - 在第五步中被挂起的第一个协程将恢复运行并释放第三步中获取的
sudog
结构体。
如流程所描述,发送者协程阻塞直至接收者就绪。但是,必要的时候,我们可以使用有缓冲channel来避免这种阻塞。
有缓冲channel
简单修改前面的例子,为channel添加缓冲,如下:
package main import ( "sync" "time" ) func main() { c := make(chan string, 2) var wg sync.WaitGroup wg.Add(2) go func() { defer wg.Done() c <- `foo` c <- `bar` }() go func() { defer wg.Done() time.Sleep(time.Second * 1) println(`Message: `+ <-c) println(`Message: `+ <-c) }() wg.Wait() }
通过这个例子,我们来分析 hchan
结构体中与缓冲相关的属性:
缓冲相关的五个属性:
-
qcount
当前缓冲中元素个数 -
dataqsize
缓冲最大数量 -
buf
指向缓冲区内存,这块内存空间可容纳dataqsize
个元素 -
sendx
缓冲区中下一个元素写入时的位置 -
recvx
缓冲区中下一个被读取的元素的位置
通过 sendx
和 recvx
,缓冲区工作机制类似于 环形队列 :
环形队列使得我们可以保证缓冲区有序,并且不需要在每次取出元素时对缓冲区重新排序。
当缓冲区满了时,向缓冲区添加元素的协程将被加入 sender
链表中,并且切换到等待状态,就像我们在上一节描述的那样。之后,当程序读取缓冲区时, recvx
位置的元素将被返回,等待状态的协程将恢复执行,它要发送的值将被存入缓冲区。这使得channel能够保证 先进先出 的特性。
缓存区不足引起的延时
创建channel时指定的缓冲区大小,可能会对性能造成巨大的影响。下面是对不同缓冲区大小的channel做的压力测试代码:
package bench import ( "sync" "sync/atomic" "testing" ) func BenchmarkWithNoBuffer(b *testing.B) { benchmarkWithBuffer(b, 0) } func BenchmarkWithBufferSizeOf1(b *testing.B) { benchmarkWithBuffer(b, 1) } func BenchmarkWithBufferSizeEqualsToNumberOfWorker(b *testing.B) { benchmarkWithBuffer(b, 5) } func BenchmarkWithBufferSizeExceedsNumberOfWorker(b *testing.B) { benchmarkWithBuffer(b, 25) } func benchmarkWithBuffer(b *testing.B, size int) { for i := 0; i < b.N; i++ { c := make(chan uint32, size) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() for i := uint32(0); i < 1000; i++ { c <- i%2 } close(c) }() var total uint32 for w := 0; w < 5; w++ { wg.Add(1) go func() { defer wg.Done() for { v, ok := <-c if !ok { break } atomic.AddUint32(&total, v) } }() } wg.Wait() } }
在这个测试程序中,包含一个生产者,向channel中发送整型元素;包含多个消费者,从channel中读取数据,并将它们原子的加入变量 total
中。
运行这个测试十次,并通过 benchstat
分析结果:
name time/op WithNoBuffer-8 306µs ± 3% WithBufferSizeOf1-8 248µs ± 1% WithBufferSizeEqualsToNumberOfWorker-8 183µs ± 4% WithBufferSizeExceedsNumberOfWorker-8 134µs ± 2%
说明合适的缓冲区大小确实会使得程序执行得更快!让我们来分析测试程序以确认耗时反生在何处。
追踪耗时
通过Go工具trace中的 synchronization blocking profile
来查看测试程序被同步原语阻塞所消耗的时间。接收时的耗时对比:无缓冲channel为9毫秒,缓冲大小为50的channel为1.9毫秒。
发送时的耗时对比:有缓冲channel将耗时缩小了五倍。
可以得出结论,缓冲区的大小确实在程序性能方面扮演了重要角色。
英文原文: Go: Buffered and Unbuffered Channels by Vincent Blanchon ( https://medium.com/@blanchon.vincent/go-buffered-and-unbuffered-channels-29a107c00268 )
Recommend
-
87
看视频总是缓冲?机器学习可能会终结这个问题蒂克伟·2017-11-06 04:40MIT 研究用机器学习减少视频缓冲时间。编者按:无论你正在观看...
-
35
-
50
go 在提供了 io 包的同时也提供了 bufio 包来实现有缓存的读写操作以提高读写性能。为什么 bufio 性能比 io 高呢? 缓冲读写 缓冲读...
-
37
应用系统分层架构,为了加速数据访问,会把最常访问的数据,放在 缓存 (cache) 里,避免...
-
49
上篇《
-
35
我们常用的是无缓冲channel : make(chan type) 其实make() 创建chan的第二个参数可设置缓冲channel的大小。 上述语句等价于 make(chan type, 1) 即创建了一个缓冲区大小为1channel 下面看有缓冲channel的...
-
63
应用系统分层架构,为了加速数据访问,会把最常访问的数据,放在缓存(cache)里,避免每次都去访问数据库。操作系统,会有缓冲池(buffer pool)机制,避免每次访问磁盘,以加速数据的访问。MySQL作为一个存储系统,同样具有缓冲池(buffer pool)机制,以避免每次查询...
-
45
目录 Go 教程系列笔记 Interface 第一部分
-
27
华为造“芯” 还有120天缓冲期 2020年05月20日 11:54 次阅读 稿源:TechWeb...
-
11
学到了!将缓冲 channel 当做 Mutex 来使用 微博 使用《开发者头条》客户端,拥有更好的阅读体验。 立即体验
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK