前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言中常见100问题-#64 Expecting a deterministic behavior using ...

Go语言中常见100问题-#64 Expecting a deterministic behavior using ...

作者头像
数据小冰
发布2022-08-15 15:24:31
4100
发布2022-08-15 15:24:31
举报
文章被收录于专栏:数据小冰
在使用select+channel时期望确定性的结果

对select在多个通道中的行为做出错误的假设是Go开发人员常犯的的一个错误,这种错误的假设可能会导致难以识别和重现的细微错误。假设我们想要实现一个需要从两个通道接收信息的goroutine,两个通道的作用如下:

  • messageCh 通道用于处理接收的消息
  • disconnectedCh 通道用于接收断开连接通知,当收到这种断开信号时,希望从函数返回

在这两个通道中,希望messageCh优先,例如,如果发生断开连接,希望在返回之前确保已收到所有消息。也许你会写如下这样的代码来处理优先级:

代码语言:javascript
复制
for {
        select {
        case v := <-messageCh:
                fmt.Println(v)
        case <-disconnectCh:
                fmt.Println("disconnection, return")
                return
        }
}

使用select从多个通道接收数据,由于考虑到有优先级,messageCh优先,所以在case语句中,将从messageCh中接收消息写在第一个位置,disconnectCh写在第二个位置。但是这段代码是有效的吗?下面通过一个生产者发送10条消息,然后发送断开连接通知进行验证。

代码语言:javascript
复制
for i := 0; i < 10; i++ {
        messageCh <- i
}
disconnectCh <- struct{}{}

运行上述代码,messageCh是缓冲通道,下面是在我的机器上运行的输出,完整的代码见(https://github.com/ThomasMing0915/100-go-mistakes-code/tree/main/64)

代码语言:javascript
复制
0
1
2
3
4
5
6
disconnect, return

通过输出结果可以看到,只消费了7条消息,那其他3条去哪里了?是什么原因导致的?原因在于多通道时,select语句是不保证顺序的。<< Programming Language Specification>>书中有下面的表述,这里与switch语句不一样,switch语句是选择第一个匹配的。但是在select语句中,如果有多个匹配,会随机选取一个。

❝如果一个或多个通道可以进行,select会通过统一的伪随机选择一个可以进行的通道。 ❞

这种处理方法一开始可能看起来比较奇怪,但有一个解释的理由:防止可能的饥饿。假设选择的第一个可能的通道是基于代码顺序的。在这种情况下,就可能会存在这种情况。例如,由于发送者速度快,会存现只能从一个通道接收,为了防止这种情况,在语言设计的时候作者决定使用随机选择。

回到前面的程序,即使case v:= <- messageCh是源顺序中的第一个,如果messageCh和disconnectCh中都有消息,则不能保证会选择哪个,所以前面的程序输出结果是不确定的,可能收到0个消息,也有可能收到5个消息,甚至可能收到10个消息,对此语言层面没有任何保证。

如何处理上面的问题呢?有多种方法可以解决在断开连接之前接收到所有消息。如果只有一个生产者,有两种处理思路:

  • 思路一:将messageCh定义为无缓冲通道而不是缓冲通道,由于发送者goroutine阻塞直到接收者goroutine准备好,它会保证在收到来自disconnectCh的断开连接之前接收到来自messageCh的所有消息
  • 思路二:使用一个通道而不是两个通道,我们可以定义一个结构体来传递消息或断开连接信息,由于通道保证发送消息的顺序与接收消息的顺序相同,因此可以保证最后会收到断开连接消息。简单说就是通过接收到的通道信息是否为特殊的断开连接信息。

如果有多个生产者,上面的处理思路就不行了,在有多个生产者goroutine的情况下,无法保证哪个goroutine先写。因此,无论是无缓冲的通道还是单个通道,都会导致生产者goroutine之间存在竞争。这种情况,可以用下面的解决方法:

  • 从messageCh或disconnectCh其一接收信息,如果收到断开连接信号,不是直接return返回,而是将messageCh中所有的现有消息(如果有)接收完,然后再返回。下面是这种处理的程序示例。
代码语言:javascript
复制
for {
        select {
        case v := <-messageCh:
                fmt.Println(v)
        case <-disconnectCh:
                for {
                        select {
                        case v := <-messageCh:
                                fmt.Println(v)
                        default:
                                fmt.Println("disconnection, return")
                                return
                        }
                }
        }
}

上述代码使用了两层for+select结构,外层的for+select处理messageCh有消息且disconnectCh没有消息的情况,内层的for+select处理收到了断开消息之后,仅当其他情况都不匹配时,才会选择select语句中的default执行,这能保证我们只有在收到messageCh中所有的剩余消息后才会返回。

下面通过一个可视化的演示来看看上述代码是如何运行的。演示的是messageCh中有两条消息和disconnectCh中有一条断开连接消息的情况。

在上图中,由于messageCh和disconnectCh中都有消息,所以会随机选择其中一个通道,现在假定选择执行的是第二通道。

现在程序会从disconnectCh中接收消息,进入内部的for+select语句中。

只要messageCh中有数据,select语句总是会执行第一个case,而不是default语句,直到接收完messageCh通道中的全部数据,才会进入default.

最后,当从messageCh接收完全部数据之后,select语句不会被block,而是选择default分支执行。

这种方法可以确保在具有多个通道的情况下,接收者可以从通道接收完所有剩余消息。当然,如果在goroutine返回之后发送消息到messageCh(例如在有多个生产者goroutine的时候),是收不到后续消息的。

总结,当select语句中有多个通道时,需要注意到,选择哪个通道是不确定的,并不是代码中写在前面的优先于后面的,因为会随机选择。在单个生产者goroutine的情况,解决这种问题的方法是使用无缓冲通道或者使用单个通道。在有多个生产者goroutine的情况下,可以使用双层for+select处理,内层for+select语句结合default处理优先级。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 数据小冰 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 在使用select+channel时期望确定性的结果
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档