避免嵌套的SELECT语句和重复的CASE模式。

huangapple go评论83阅读模式
英文:

Pattern to avoid nested selects with repeated cases

问题

我正在阅读O'Reilly的《Go并发编程》一书,并找到了这个代码示例:

doWork := func(
	done <-chan interface{},
	pulseInterval time.Duration,
) (<-chan interface{}, <-chan time.Time) {
	heartbeat := make(chan interface{})
	results := make(chan time.Time)
	go func() {
		defer close(heartbeat)
		defer close(results)
		pulse := time.Tick(pulseInterval)
		workGen := time.Tick(2 * pulseInterval) // this just creates workload
		sendPulse := func() {
			select {
			case heartbeat <- struct{}{}:
			default:
			}
		}
		sendResult := func(r time.Time) {
			for {
				select {
				case <-done:
					return
				case <-pulse:
					sendPulse()
				case results <- r:
					return
				}
			}
		}
		for {
			select {
			case <-done:
				return
			case <-pulse:
				sendPulse()
			case r := <-workGen:
				sendResult(r)
			}
		}
	}()
	return heartbeat, results
}

这段代码中使用了两个select语句来实现简单的心跳功能,这似乎很奇怪。后来我明白了这样做的原因是,sendResult函数不应该阻塞尝试向results通道发送结果。如果没有人从该通道接收数据,它将有效地阻塞goroutine并停止发送心跳信号。

然后我想...为什么他们不这样编写呢?

doWork := func(
	done <-chan interface{},
	pulseInterval time.Duration,
) (<-chan interface{}, <-chan time.Time) {
	heartbeat := make(chan interface{})
	results := make(chan time.Time)
	go func() {
		defer close(heartbeat)
		defer close(results)
		pulse := time.Tick(pulseInterval)
		workGen := time.Tick(2 * pulseInterval) // this just creates workload
		sendPulse := func() {
			select {
			case heartbeat <- struct{}{}:
			default:
			}
		}
		for {
			select {
			case <-done:
				return
			case <-pulse:
				sendPulse()
			case results <- <-workgen:
				fmt.Println("Send result")
			}
		}
	}()
	return heartbeat, results
}

然后我意识到,如果workgen有可用的项并且我读取了它,但是没有人从results中读取,那么workgen的项将丢失。

问题:

有没有其他模式可以避免这些嵌套的select语句,而不必重复所有的case以避免丢失值?

思考:

如果select语句首先验证results通道是否等待接收值,而不是首先执行右侧表达式,那么所有这些问题都会得到解决,不是吗?

换句话说,如果我有这样的语句results <- <-workgen,如果select语句能够判断"嘿,没有人从results中读取数据,我们不要尝试评估<-workgen",那就更好了。对我来说,这似乎是一个非常特殊的设计选择,我认为这就是为什么需要很多嵌套的select语句的原因。

我认为一个case应该首先尝试评估表达式中的通道是否准备好接收或发送数据,然后再执行表达式。

英文:

I'm reading the Concurrency in Go book from O'Reilly and found this code example:

doWork := func(
	done &lt;-chan interface{},
	pulseInterval time.Duration,
) (&lt;-chan interface{}, &lt;-chan time.Time) {
	heartbeat := make(chan interface{})
	results := make(chan time.Time)
	go func() {
		defer close(heartbeat)
		defer close(results)
		pulse := time.Tick(pulseInterval)
		workGen := time.Tick(2 * pulseInterval) // this just creates workload
		sendPulse := func() {
			select {
			case heartbeat &lt;- struct{}{}:
			default:
			}
		}
		sendResult := func(r time.Time) {
			for {
				select {
				case &lt;-done:
					return
				case &lt;-pulse:
					sendPulse()
				case results &lt;- r:
					return
				}
			}
		}
		for {
			select {
			case &lt;-done:
				return
			case &lt;-pulse:
				sendPulse()
			case r := &lt;-workGen:
				sendResult(r)
			}
		}
	}()
	return heartbeat, results
}

It seemed very strange that two select statements were being used for a simple heartbeat. Then I understood that the reason behind this is that sendResults SHOULDN'T block trying to send a result to the results channel. If nobody is receiving from that channel, it will effectively block the goroutine and stop sending the heartbeat.

Then I thought... Why didn't they code it like this then?

doWork := func(
    done &lt;-chan interface{},
    pulseInterval time.Duration,
) (&lt;-chan interface{}, &lt;-chan time.Time) {
    heartbeat := make(chan interface{})
    results := make(chan time.Time)
    go func() {
        defer close(heartbeat)
        defer close(results)
        pulse := time.Tick(pulseInterval)
        workGen := time.Tick(2 * pulseInterval) // this just creates workload
        sendPulse := func() {
            select {
            case heartbeat &lt;- struct{}{}:
            default:
            }
        }
        for {
            select {
            case &lt;-done:
                return
            case &lt;-pulse:
                sendPulse()
            case results &lt;- &lt;- workgen:
                fmt.Println(&quot;Send result&quot;)
            }
        }
    }()
    return heartbeat, results
}

And realized that it is because if workgen has an available item and I read it but then nobody is reading from results, the workgen item will be lost.

Question

Is there any other pattern to avoid these nested selects that have to repeat all the cases not to miss a value?

Thoughts

Wouldn't all these problems be solved IF a select statement first validated that the results channel is waiting to receive a value instead of executing the right hand expression first?

In other words, if I have this results &lt;- &lt;- workgen, it would be better if the select said "hey, nobody is reading from results, let's not try to evaluate <- workgen". This seems like a very particular design choice for me and I think it is what makes a lot of nested selects necessary to begin with.

I think a case should first try to evaluate if the channels that are in the expression are ready to receive or send data, and then execute the expressions.

答案1

得分: 1

在选择语句中,为workgenresults使用临时变量。根据当前状态,将这些变量设置为nil或原始值。

将变量设置为nil会禁用选择语句中的分支,因为对nil通道的通道操作会永远阻塞。

workGenT := workGen
var resultsT chan time.Time
var result time.Time
for {
    select {
    case <-done:
        return
    case <-pulse:
        sendPulse()
    case result = <-workgenT:
        workgenT = nil
        resultsT = results
    case resultsT <- result:
        resultsT = nil
        workgenT = workgen
    }
}

因为只有workgenTresultsT中的一个是非nil的,上述代码有两个状态:程序要么在等待接收结果,要么在等待发送结果。

英文:

Use temporary variables for workgen and results in the select. Set these variables to nil or the original values depending on the current state.

Setting the variables to nil disables the branch of the select because channel operations on nil channels block forever.

	workGenT := workGen
	var resultsT chan time.Time
	var result time.Time
	for {
		select {
		case &lt;-done:
			return
		case &lt;-pulse:
			sendPulse()
		case result = &lt;-workgenT:
			workgenT = nil
			resultsT = results
		case resultsT &lt;- result:
			resultsT = nil
			workgenT = workgen
		}
	}

Because only one of workgenT and resultsT are non-nil, the code above has two states: The program is either waiting to receive a result or waiting to send a result.

答案2

得分: 1

另一个简单且类似的模式。

func hBeatTicker(ctx context.Context, ch chan<- struct{}, t time.Duration) {
	tick := time.NewTicker(t)
	for {
		select {
		case <-ctx.Done():
			close(ch)
			tick.Stop()
			return
		case <-tick.C:
			ch <- struct{}{}
		}
	}
}

func workGen(ctx context.Context, ch chan<- time.Time, t time.Duration) {
	tick := time.NewTicker(2 * t)
	for {
		select {
		case <-ctx.Done():
			close(ch)
			tick.Stop()
			return
		case ch <- <-tick.C:
		}
	}
}

func do(ctx context.Context, d time.Duration) (<-chan struct{}, <-chan time.Time) {
	heartbeat, results := make(chan struct{}), make(chan time.Time)

	go workGen(ctx, results, d)
	go hBeatTicker(ctx, heartbeat, d)

	return heartbeat, results
}
英文:

Another simple and similar pattern.

func hBeatTicker(ctx context.Context, ch chan&lt;- struct{}, t time.Duration) {
	tick := time.NewTicker(t)
	for {
		select {
		case &lt;-ctx.Done():
			close(ch)
			tick.Stop()
			return
		case &lt;-tick.C:
			ch &lt;- struct{}{}
		}
	}
}

func workGen(ctx context.Context, ch chan&lt;- time.Time, t time.Duration) {
	tick := time.NewTicker(2 * t)
	for {
		select {
		case &lt;-ctx.Done():
			close(ch)
			tick.Stop()
			return
		case ch &lt;- &lt;-tick.C:
		}
	}
}

func do(ctx context.Context, d time.Duration) (&lt;-chan struct{}, &lt;-chan time.Time) {
	heartbeat, results := make(chan struct{}), make(chan time.Time)

	go workGen(ctx, results, d)
	go hBeatTicker(ctx, heartbeat, d)

	return heartbeat, results
}

huangapple
  • 本文由 发表于 2021年6月16日 02:02:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/67991378.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定