如何有效利用渠道
How to use channels efficiently
我在 Uber's style guide 上读到最多应该使用 1 的通道长度。
虽然我很清楚使用 100 或 1000 的通道大小是非常糟糕的做法,但是 我想知道为什么 10 的通道大小不被视为有效选项。我遗漏了一些无法得出正确结论的部分。
下面,您可以遵循我的一些基准测试支持的论点(和反论点)。
我明白,如果你的两个 go-routines,负责写入或从这个通道读取,将在顺序写入或读取之间被中断 to/from 通道被其他一些 IO 操作打断,没有好处预期来自更高的通道缓冲区,我同意 1 是最佳选择。
但是,可以说除了由 writing/reading to/from 通道引起的隐式锁定和解锁之外,不需要其他重要的 go-routine 切换。然后我会得出以下结论:
考虑使用大小为 1 和 10 的通道缓冲区(GR = go-routine)在通道上处理 100 个值时上下文切换的数量
- Buffer=1:(GR1插入1个值,GR2读取1个值)X 100 ~ 200 go-routine switches
- Buffer=10: (GR1插入10个值,GR2读取10个值) X 10 ~ 20 go-routine switches
我做了一些基准测试来证明这实际上运行得更快:
package main
import (
"testing"
)
type a struct {
b [100]int64
}
func BenchmarkBuffer1(b *testing.B) {
count := 0
c := make(chan a, 1)
go func() {
for i := 0; i < b.N; i++ {
c <- a{}
}
close(c)
}()
for v := range c {
for i := range v.b {
count += i
}
}
}
func BenchmarkBuffer10(b *testing.B) {
count := 0
c := make(chan a, 10)
go func() {
for i := 0; i < b.N; i++ {
c <- a{}
}
close(c)
}()
for v := range c {
for i := range v.b {
count += i
}
}
}
简单读写+非阻塞处理对比结果:
BenchmarkBuffer1-12 5072902 266 ns/op
BenchmarkBuffer10-12 6029602 179 ns/op
PASS
BenchmarkBuffer1-12 5228782 256 ns/op
BenchmarkBuffer10-12 5392410 216 ns/op
PASS
BenchmarkBuffer1-12 4806208 287 ns/op
BenchmarkBuffer10-12 4637842 233 ns/op
PASS
但是,如果我每读取 10 次就睡眠一次,它不会产生更好的结果。
import (
"testing"
"time"
)
func BenchmarkBuffer1WithSleep(b *testing.B) {
count := 0
c := make(chan int, 1)
go func() {
for i := 0; i < b.N; i++ {
c <- i
}
close(c)
}()
for a := range c {
count++
if count%10 == 0 {
time.Sleep(time.Duration(a) * time.Nanosecond)
}
}
}
func BenchmarkBuffer10WithSleep(b *testing.B) {
count := 0
c := make(chan int, 10)
go func() {
for i := 0; i < b.N; i++ {
c <- i
}
close(c)
}()
for a := range c {
count++
if count%10 == 0 {
time.Sleep(time.Duration(a) * time.Nanosecond)
}
}
}
每 10 次读取添加一次睡眠的结果:
BenchmarkBuffer1WithSleep-12 856886 53219 ns/op
BenchmarkBuffer10WithSleep-12 929113 56939 ns/op
仅供参考:我也只用一个 CPU 再次进行了测试,得到以下结果:
BenchmarkBuffer1 5831193 207 ns/op
BenchmarkBuffer10 6226983 180 ns/op
BenchmarkBuffer1WithSleep 556635 35510 ns/op
BenchmarkBuffer10WithSleep 984472 61434 ns/op
绝对没有任何问题 上限为 500 的频道例如如果此通道用作信号量。
您阅读的风格指南建议不要使用比方说上限 64 的缓冲通道,“因为这看起来是一个不错的数字”。但是这个推荐是不是因为性能! (顺便说一句:您的微基准测试是无用的微基准测试,它们不衡量任何相关内容。)
无缓冲通道是某种同步原语,对我们来说非常有用。
一个缓冲通道,好吧,可以在发送者和接收者之间缓冲,这种缓冲对于观察、调整和调试代码(因为创建和消费进一步解耦)。这就是 style 指南推荐无缓冲通道的原因(或最多 1 个上限,因为有时需要这样做才能确保正确性!)。
它也不禁止更大的缓冲区上限:
Any other [than 0 or 1] size must be subject to a high level of scrutiny. Consider how the size is determined, what prevents the channel from filling up under load and blocking writers, and what happens when this occurs. [emph. mine]
如果可以解释为什么是 27(而不是 22 或 31)以及这将如何影响程序行为[=33,则可以使用 27 的上限=](不仅仅是性能!)如果缓冲区已满。
大多数人都高估了表现。正确性、运行稳定性和可维护性是第一位的。这就是本风格指南的内容。
我在 Uber's style guide 上读到最多应该使用 1 的通道长度。
虽然我很清楚使用 100 或 1000 的通道大小是非常糟糕的做法,但是 我想知道为什么 10 的通道大小不被视为有效选项。我遗漏了一些无法得出正确结论的部分。
下面,您可以遵循我的一些基准测试支持的论点(和反论点)。
我明白,如果你的两个 go-routines,负责写入或从这个通道读取,将在顺序写入或读取之间被中断 to/from 通道被其他一些 IO 操作打断,没有好处预期来自更高的通道缓冲区,我同意 1 是最佳选择。
但是,可以说除了由 writing/reading to/from 通道引起的隐式锁定和解锁之外,不需要其他重要的 go-routine 切换。然后我会得出以下结论:
考虑使用大小为 1 和 10 的通道缓冲区(GR = go-routine)在通道上处理 100 个值时上下文切换的数量
- Buffer=1:(GR1插入1个值,GR2读取1个值)X 100 ~ 200 go-routine switches
- Buffer=10: (GR1插入10个值,GR2读取10个值) X 10 ~ 20 go-routine switches
我做了一些基准测试来证明这实际上运行得更快:
package main
import (
"testing"
)
type a struct {
b [100]int64
}
func BenchmarkBuffer1(b *testing.B) {
count := 0
c := make(chan a, 1)
go func() {
for i := 0; i < b.N; i++ {
c <- a{}
}
close(c)
}()
for v := range c {
for i := range v.b {
count += i
}
}
}
func BenchmarkBuffer10(b *testing.B) {
count := 0
c := make(chan a, 10)
go func() {
for i := 0; i < b.N; i++ {
c <- a{}
}
close(c)
}()
for v := range c {
for i := range v.b {
count += i
}
}
}
简单读写+非阻塞处理对比结果:
BenchmarkBuffer1-12 5072902 266 ns/op
BenchmarkBuffer10-12 6029602 179 ns/op
PASS
BenchmarkBuffer1-12 5228782 256 ns/op
BenchmarkBuffer10-12 5392410 216 ns/op
PASS
BenchmarkBuffer1-12 4806208 287 ns/op
BenchmarkBuffer10-12 4637842 233 ns/op
PASS
但是,如果我每读取 10 次就睡眠一次,它不会产生更好的结果。
import (
"testing"
"time"
)
func BenchmarkBuffer1WithSleep(b *testing.B) {
count := 0
c := make(chan int, 1)
go func() {
for i := 0; i < b.N; i++ {
c <- i
}
close(c)
}()
for a := range c {
count++
if count%10 == 0 {
time.Sleep(time.Duration(a) * time.Nanosecond)
}
}
}
func BenchmarkBuffer10WithSleep(b *testing.B) {
count := 0
c := make(chan int, 10)
go func() {
for i := 0; i < b.N; i++ {
c <- i
}
close(c)
}()
for a := range c {
count++
if count%10 == 0 {
time.Sleep(time.Duration(a) * time.Nanosecond)
}
}
}
每 10 次读取添加一次睡眠的结果:
BenchmarkBuffer1WithSleep-12 856886 53219 ns/op
BenchmarkBuffer10WithSleep-12 929113 56939 ns/op
仅供参考:我也只用一个 CPU 再次进行了测试,得到以下结果:
BenchmarkBuffer1 5831193 207 ns/op
BenchmarkBuffer10 6226983 180 ns/op
BenchmarkBuffer1WithSleep 556635 35510 ns/op
BenchmarkBuffer10WithSleep 984472 61434 ns/op
绝对没有任何问题 上限为 500 的频道例如如果此通道用作信号量。
您阅读的风格指南建议不要使用比方说上限 64 的缓冲通道,“因为这看起来是一个不错的数字”。但是这个推荐是不是因为性能! (顺便说一句:您的微基准测试是无用的微基准测试,它们不衡量任何相关内容。)
无缓冲通道是某种同步原语,对我们来说非常有用。
一个缓冲通道,好吧,可以在发送者和接收者之间缓冲,这种缓冲对于观察、调整和调试代码(因为创建和消费进一步解耦)。这就是 style 指南推荐无缓冲通道的原因(或最多 1 个上限,因为有时需要这样做才能确保正确性!)。
它也不禁止更大的缓冲区上限:
Any other [than 0 or 1] size must be subject to a high level of scrutiny. Consider how the size is determined, what prevents the channel from filling up under load and blocking writers, and what happens when this occurs. [emph. mine]
如果可以解释为什么是 27(而不是 22 或 31)以及这将如何影响程序行为[=33,则可以使用 27 的上限=](不仅仅是性能!)如果缓冲区已满。
大多数人都高估了表现。正确性、运行稳定性和可维护性是第一位的。这就是本风格指南的内容。