在选择中支持一种通信(chan)
Posted
技术标签:
【中文标题】在选择中支持一种通信(chan)【英文标题】:Favor one communication (chan) in select 【发布时间】:2020-11-21 19:18:35 【问题描述】:我知道,如果在select
语句中可以进行多个“通信”,则随机选择一个。我正在尝试寻找一种替代方法,它可以更喜欢一种“交流”而不是另一种。
背景是我在一个使用上下文杀死的通道上的 go-routine 中发送值。当我杀死它时,我希望立即关闭通道,但目前代码有时会在关闭之前在通道上发送最终值。
下面是简化版的代码:
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan int)
go func()
defer close(ch)
for i := 1; ; i++
select
case <-ctx.Done():
return
case ch <- i:
()
print(<-ch)
print(<-ch)
cancel()
print(<-ch)
print(<-ch)
这有时会打印 1200,但通常会打印 1230。Try it on the playground
对于如何重组代码以支持第一种情况有什么想法吗? (即总是打印 1200。)
【问题讨论】:
这是非常不寻常的代码。通常在取消上下文后您不会收到来自 ch 的信息。无论如何,您没有理由期望在此处打印任何特定值。我认为这不是真实代码的有用近似值。 @Peter,这有点类似于Timer.Reset()
的情况
@BurakSerdar “只有在停止或过期且通道已耗尽的计时器上才应调用重置。”从 time.Timer.Reset 文档开始。
@Peter,我相信这实际上是一个很好的近似值。的真实代码。实际上,您目前必须在调用cancel()
后继续读取chan
,否则写入它的goroutine 可能 阻塞并且永远不会退出。顺便说一句,我宁愿在取消后不必阅读 chan,这就是这个问题的重点。
不,goroutine 不会阻塞,因为 ctx.Done() 情况可用。
【参考方案1】:
请注意,这是一个更新的答案,因为原始答案存在问题。
正如其他人指出的那样,如果没有额外的同步,您将无法避免竞争条件。您可以使用 Mutex,但 sync.Cond
似乎很合适。
在下面的代码中,接收 goroutine 表示它已从 chan 接收到值。它在发出信号之前取消上下文(使用Cond.Signal
),发送goroutine 等待信号。这避免了竞争条件,因为上下文状态在检查之前已更新。
ctx, cancel := context.WithCancel(context.Background())
ch := make(chan int)
cond := sync.NewCond(&sync.Mutex) // *** new
go func()
defer close(ch)
cond.L.Lock() // *** new
defer cond.L.Unlock() // *** new
for i := 1; ; i++
ch <- i // *** moved
cond.Wait() // *** new
if ctx.Err() != nil // *** changed
return
()
print(<-ch)
cond.Signal() // *** new
print(<-ch)
cond.Signal() // *** new
print(<-ch)
cancel()
cond.Signal() // *** new
print(<-ch)
cond.Signal() // *** new
这是我能看到的最简单的方法,接收 goroutine 在取消上下文后将不会在通道上接收任何值。
在Playground上试试
【讨论】:
这不行,可以在等待写入通道时取消上下文。 这是“更好”的,因为通常会打印 1200,但正如 Burak 所说,这不是解决方案 - 例如:在取消之前添加睡眠 play.golang.org/p/T6fEX2ZGCHJ 所提供的答案被标记为低质量帖子以供审核。以下是How do I write a good answer? 的一些指南。提供的这个答案可能是正确的,但它可以从解释中受益。仅代码答案不被视为“好”答案。 感谢 cmets。我添加了解释并修复了代码以实现 OP 试图做的事情。【参考方案2】:这似乎是不可能的,因为cancel()
不是主 goroutine 中的阻塞操作。正因为如此,当select
解除阻塞时,可能会有多种情况可用,并且没有办法让一个通道优于另一个通道。任何类型的 check-channel-then-write 方案都将是活泼的,因为在检查后可以取消上下文。
使用done
通道并写入它而不是取消上下文将起作用,因为写入done
通道将是主 goroutine 的阻塞操作,并且 select 将始终有一个活动案例。
【讨论】:
谢谢 Burak,我已经理解了你的第一段,这就是为什么我要询问有关如何重组代码的想法。我会尝试完成的chan
,但我希望有一种方法可以使用context
。
我开始将这个问题的答案写成“这就是你的做法......”后来变成了我们上面所说的。当我想得更多时,我相信由于select
的工作方式以及cancel()
的效果不会立即被goroutine 知道(即cancel()
确实不唤醒 goroutine)以上是关于在选择中支持一种通信(chan)的主要内容,如果未能解决你的问题,请参考以下文章