【问题标题】:How to use channels efficiently [closed]如何有效地使用渠道[关闭]
【发布时间】:2021-05-07 09:58:39
【问题描述】:

我在Uber's style guide 上读到,最多应该使用 1 的通道长度。

虽然我很清楚使用 100 或 1000 的通道大小是非常糟糕的做法,但 我想知道为什么 10 的通道大小不被视为有效选项。我遗漏了一些部分来得出正确的结论

下面,您可以按照我的一些基准测试支持的论点(和反论点)。

我了解,如果您负责从该通道写入或读取的两个 go-routines 在顺序写入或读取到/从通道之间被某些其他 IO 操作中断,则不会预期任何收益更高的通道缓冲区,我同意 1 是最佳选择。

但是,可以说除了由通道写入/读取引起的隐式锁定和解锁之外,不需要其他重要的 go-routine 切换。然后我会得出以下结论:

考虑在通道缓冲区大小为 1 和 10(GR = go-routine)的通道上处理 100 个值时上下文切换的数量

  • Buffer=1:(GR1 插入 1 个值,GR2 读取 1 个值)X 100 ~ 200 个 go-routine 切换
  • Buffer=10:(GR1 插入 10 个值,GR2 读取 10 个值)X 10 ~ 20 个 go-routine 切换

我做了一些基准测试来证明这实际上更快:

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

【问题讨论】:

  • 一个使用大通道缓冲区有问题的例子:你不想在通道上发送阻塞,所以你给它一个足够大的缓冲区,这样它“不应该”得到填满了。
  • 大小为 10 的通道是一个完全有效的选项——对于某些操作。事实上,它是某些操作的唯一选择。

标签: performance go optimization benchmarking channels


【解决方案1】:

绝对没有上限为 500 的频道有问题,例如如果此通道用作信号量。

您阅读的样式指南建议不要使用缓冲通道,比如说 cap 64,“因为这看起来是一个不错的数字”。但是这个建议不是因为性能! (顺便说一句:你们的微基准是无用的微基准,它们不会测量任何相关的东西。)

无缓冲通道是某种同步原语,对我们非常有用。

一个缓冲的通道,嗯,可以在发送者和接收者之间缓冲,这种缓冲对于观察调整调试代码可能是有问题的(因为创造和消费进一步脱钩)。这就是 style 指南推荐无缓冲通道的原因(或最多为 1 的上限,因为有时需要这样做以确保正确性!)。

它也不禁止更大的缓冲区上限:

任何 [0 或 1 以外的] 尺寸都必须经过高度审查。考虑如何确定大小,防止通道在负载下填满和阻塞写入器,以及发生这种情况时会发生什么。 [嗯。我的]

如果您可以解释为什么 27(而不是 22 或 31)以及这将如何影响程序行为(不仅仅是性能!),您可以使用 27 的上限如果缓冲区已满。

大多数人都高估了性能。正确性、运行稳定性和可维护性是第一位的。这就是本风格指南的内容。

【讨论】:

  • 非常感谢@Volker。这很清楚。
猜你喜欢
  • 2021-09-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-07-04
  • 2019-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多