英文:
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 <-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
}
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 <-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
}
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 <- <- 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
在选择语句中,为workgen
和results
使用临时变量。根据当前状态,将这些变量设置为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
}
}
因为只有workgenT
和resultsT
中的一个是非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 <-done:
return
case <-pulse:
sendPulse()
case result = <-workgenT:
workgenT = nil
resultsT = results
case resultsT <- 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<- 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
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论