我今天发现了一些让我有点困惑的东西,我想让社区来运行它,看看我是否遗漏了什么,或者可能只是设计得很糟糕.

用例:我有一个输入通道,我想要一个Go routine 来等待该通道上的值.如果取消了上下文,请退出.(可选)如果某个输入等待了一定时间而没有收到,也可以运行回调.

我从这样的代码开始:

func myRoutine(ctx context.Context, c <-chan int, callbackInterval *time.Duration, callback func() error) {
    var timeoutChan <-chan time.Time
    var timer *time.Timer
    if callbackInterval != nil {
        // If we have a callback interval set, create a timer for it
        timer = time.NewTimer(*callbackInterval)
        timeoutChan = timer.C
    } else {
        // If we don't have a callback interval set, create
        // a channel that will never provide a value.
        timeoutChan = make(<-chan time.Time, 0)
    }

    for {
        select {

        // Handle context cancellation
        case <-ctx.Done():
            return

        // Handle timeouts
        case <-timeoutChan:
            callback()

        // Handle a value in the channel
        case v, ok := <-c:
            if !ok {
                // Channel is closed, exit out
                return
            }

            // Do something with v
            fmt.Println(v)
        }

        // Reset the timeout timer, if there is one
        if timer != nil {
            if !timer.Stop() {
                // See documentation for timer.Stop() for why this is needed
                <-timer.C
            }
            // Reset the timer
            timer.Reset(*callbackInterval)
            timeoutChan = timer.C
        }
    }
}

这种设计看起来很好,因为(据我所知)在select中没有条件case(我认为reflect是可能的,但这通常是超慢的),所以没有两个不同的selects(需要计时器时一个有计时器,一个没有)和if来 Select ,我刚刚做了一个select,如果不需要计时器,计时器通道永远不会提供值.保持干燥.

但后来我开始怀疑这对性能的影响.当我们不使用计时器时,如果在select中有一个从未获得值的额外通道(代替计时器通道),会不会减慢应用程序的速度?

所以,我决定做一些测试来进行比较.

package main

import (
    "context"
    "fmt"
    "reflect"
    "time"
)

func prepareChan() chan int {
    var count int = 10000000

    c := make(chan int, count)

    for i := 0; i < count; i++ {
        c <- i
    }
    close(c)
    return c
}

func oneChan() int64 {
    c := prepareChan()

    foundVal := true
    start := time.Now()
    for {
        select {
        case _, foundVal = <-c:
            break
        }
        if !foundVal {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("1 Chan - Standard: %dms\n", ms)
    return ms
}

func twoChan() int64 {
    c := prepareChan()

    neverchan1 := make(chan struct{}, 0)

    foundVal := true
    start := time.Now()
    for {
        select {
        case _, foundVal = <-c:
            break
        case <-neverchan1:
            break
        }
        if !foundVal {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("2 Chan - Standard: %dms\n", ms)
    return ms
}

func threeChan() int64 {
    c := prepareChan()

    neverchan1 := make(chan struct{}, 0)
    neverchan2 := make(chan struct{}, 0)

    foundVal := true
    start := time.Now()
    for {
        select {
        case _, foundVal = <-c:
            break
        case <-neverchan1:
            break
        case <-neverchan2:
            break
        }
        if !foundVal {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("3 Chan - Standard: %dms\n", ms)
    return ms
}

func fourChan() int64 {
    c := prepareChan()

    neverchan1 := make(chan struct{}, 0)
    neverchan2 := make(chan struct{}, 0)
    neverchan3 := make(chan struct{}, 0)

    foundVal := true
    start := time.Now()
    for {
        select {
        case _, foundVal = <-c:
            break
        case <-neverchan1:
            break
        case <-neverchan2:
            break
        case <-neverchan3:
            break
        }
        if !foundVal {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("4 Chan - Standard: %dms\n", ms)
    return ms
}

func oneChanReflect() int64 {
    c := reflect.ValueOf(prepareChan())

    branches := []reflect.SelectCase{
        {Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
    }

    start := time.Now()
    for {
        _, _, recvOK := reflect.Select(branches)
        if !recvOK {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("1 Chan - Reflect: %dms\n", ms)
    return ms
}

func twoChanReflect() int64 {
    c := reflect.ValueOf(prepareChan())
    neverchan1 := reflect.ValueOf(make(chan struct{}, 0))

    branches := []reflect.SelectCase{
        {Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
        {Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
    }

    start := time.Now()
    for {
        _, _, recvOK := reflect.Select(branches)
        if !recvOK {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("2 Chan - Reflect: %dms\n", ms)
    return ms
}

func threeChanReflect() int64 {
    c := reflect.ValueOf(prepareChan())
    neverchan1 := reflect.ValueOf(make(chan struct{}, 0))
    neverchan2 := reflect.ValueOf(make(chan struct{}, 0))

    branches := []reflect.SelectCase{
        {Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
        {Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
        {Dir: reflect.SelectRecv, Chan: neverchan2, Send: reflect.Value{}},
    }

    start := time.Now()
    for {
        _, _, recvOK := reflect.Select(branches)
        if !recvOK {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("3 Chan - Reflect: %dms\n", ms)
    return ms
}

func fourChanReflect() int64 {
    c := reflect.ValueOf(prepareChan())
    neverchan1 := reflect.ValueOf(make(chan struct{}, 0))
    neverchan2 := reflect.ValueOf(make(chan struct{}, 0))
    neverchan3 := reflect.ValueOf(make(chan struct{}, 0))

    branches := []reflect.SelectCase{
        {Dir: reflect.SelectRecv, Chan: c, Send: reflect.Value{}},
        {Dir: reflect.SelectRecv, Chan: neverchan1, Send: reflect.Value{}},
        {Dir: reflect.SelectRecv, Chan: neverchan2, Send: reflect.Value{}},
        {Dir: reflect.SelectRecv, Chan: neverchan3, Send: reflect.Value{}},
    }

    start := time.Now()
    for {
        _, _, recvOK := reflect.Select(branches)
        if !recvOK {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("4 Chan - Reflect: %dms\n", ms)
    return ms
}

func main() {
    oneChan()
    oneChanReflect()
    twoChan()
    twoChanReflect()
    threeChan()
    threeChanReflect()
    fourChan()
    fourChanReflect()
}

结果是:

1 Chan - Standard: 169ms
1 Chan - Reflect: 1017ms
2 Chan - Standard: 460ms
2 Chan - Reflect: 1593ms
3 Chan - Standard: 682ms
3 Chan - Reflect: 2041ms
4 Chan - Standard: 950ms
4 Chan - Reflect: 2423ms

它随通道数线性扩展.事后来看,我认为这是有道理的,因为它必须做一个快速循环来轮询每个通道,看看它是否有一个值?正如所料,使用reflect要慢得多.

无论如何,我的问题是:

  1. Does this result surprise anyone else? I would have expected it would use a interrupt-based design that would allow it to maintain the same performance regardless of the number of channels in the select, as it wouldn't need to poll each channel.

  2. Given the original problem that I was trying to solve (an "optional" case in a select), what would be the best/preferred design? Is the answer just to have two different selects, one with the timer and one without? That gets awfully messy when I have 2 or 3 conditional/optional timers for various things.

EDIT: @Brits suggested using a nil channel for "never returning a value" instead of an initialized channel, i.e. using var neverchan1 chan struct{} instead of neverchan1 := make(chan struct{}, 0). Here are the new performance results:

1 Chan - Standard: 221ms
1 Chan - Reflect: 1639ms
2 Chan - Standard: 362ms
2 Chan - Reflect: 2544ms
3 Chan - Standard: 376ms
3 Chan - Reflect: 3359ms
4 Chan - Standard: 394ms
4 Chan - Reflect: 4123ms

There's still an effect, most noticeably from one channel in the select to two, but after the second one the performance impact is much smaller than with an initialized channel.

Still wondering if this is the best possible solution though...

推荐答案

As per the comments an alternative to using select with a channel that "channel never provides a value" is to use a nil channel ("never ready for communication"). Replacing neverchan1 := make(chan struct{}, 0) with var neverchan1 chan struct{} (or neverchan1 := chan struct{}(nil)) as per the following example:

func twoChan() int64 {
    c := prepareChan()

    var neverchan1 chan struct{} // was neverchan1 := make(chan struct{}, 0)

    foundVal := true
    start := time.Now()
    for {
        select {
        case _, foundVal = <-c:
            break
        case <-neverchan1:
            break
        }
        if !foundVal {
            break
        }
    }
    ms := time.Since(start).Milliseconds()
    fmt.Printf("2 Chan - Standard: %dms\n", ms)
    return ms
}

This significantly narrows the gap (using 4 channel version as the difference is greater - my machine is a bit slower than yours):

4 Chan - Standard: 1281ms
4 Chan - Nil: 394ms

is the best possible solution

No; but that would probably involve some assembler! There are a number things you could do that may improve on this (here are a few very rough examples); however their effectiveness is going to depend on a range of factors (real life vs contrived test case performance often differs significantly).

At this point I would be asking "what impact is optimising this function going to have on the overall application?"; unless saving a few ns will make a material difference (i.e. improve profit!) I'd suggest stopping at this point until you:

Go相关问答推荐

Golang regexpp:获取带有右括号的单词

无法使用exec从管道中读取.Go中的命令

go mod tidy会自动升级go.mod中的go版本吗?

Golang校验器包:重命名字段错误处理

如何将验证器标记添加到嵌套字段

MQTT 客户端没有收到另一个客户端发送的消息

如何在模板中传递和访问 struct 片段和 struct

在嵌套模板中使用变量,它也被定义为 go 模板中的变量?

转换朴素递归硬币问题时的记忆错误

为什么 `append(x[:0:0], x...)` 将切片复制到 Go 中的新后备数组中?

从数据库中带有 imageurl 的文件夹中获取图像,并在我的浏览器中用 golang 中的 echo 显示

如何将一片十六进制字节转换为整数

如何通过组合来自不同包的接口来创建接口?

curl:(56)Recv失败:连接由golang中的对等方与docker重置

Golang - 无法从 pipped Windows 命令中获取结果

Ginkgo/Gomega panic 测试失败

手动将 OpenTelemetry 上下文从 golang 提取到字符串中?

无法识别同步错误.使用一次

gopls 为 github.com/Shopify/sarama 返回错误gopls: no packages returned: packages.Load error

如何使用 httputil.ReverseProxy 设置 X-Forwarded-For