在选择中支持一种通信(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)的主要内容,如果未能解决你的问题,请参考以下文章

Go语言通道(chan)——goroutine之间通信的管道

RSocket云原生架构下的另一种通信协议选择

go channel 概述

后台 UWP 网络通信

进程间通信

如何通过wifi iOS和android之间进行通信?