我今天发现了一些让我有点困惑的东西,我想让社区来运行它,看看我是否遗漏了什么,或者可能只是设计得很糟糕.
用例:我有一个输入通道,我想要一个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
要慢得多.
无论如何,我的问题是:
-
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. -
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 differentselect
s, 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...