观《从Go channel中批量读取数据》有感

鸟叔原文:从Go channel中批量读取数据

看完文章之后,感觉有一些问题没有弄明白。

  1. 有default 的版本 和 没有default的版本有什么区别?好像只是把case的内容重新写在了default里面

  2. 那为什么需要使用default,如何区分default是否会导致循环空转,大量占用CPU

  3. 为什么<-time.After(100 * time.Millisecond) 的实现会带来大量的Timer 不能及时被回收?

下面开始解决我的疑问,首先想一下为什么需要有default:

请看代码:

go 复制代码
package main

import (
        "fmt"
        "time"
)

func main() {
        ch := make(chan int, 10)
        // 读取chan
        go func() {
                for {
                        select {
                        case i := <-ch:
                                // 只读取15次chan
                                fmt.Println(i)
                        default:
                                // 读取15次chan以后的操作一直在这个空语句无任何IO操作的default条件里死循环,无法出让P,以保证一个GPM关系。
                                // 而如果无default条件的话,则系统当读取完15次chan后,当前goroutine会发生 chan IO 阻塞, Go调度器根据GPM的调度关系,会将当前执行关系中的G切换出去,再从LRQ队列中取一个新的G,重新组成一个GPM继续执行,以实现合理利用计算机资源,提高GO的高并发性能
                        }
                }
        }()
        // 写入10个值到chan
        for i := 0; i < 15; i++ {
                ch <- i
        }
        // 模拟程序效果使用
        time.Sleep(time.Minute)
}

这段代码有什么问题?

程序运行时,先使用go关键字创建一个 goroutine,里面是一个 for 循环语句。for 语句里面通过 select{} 来监听是否有 chan 的 IO 操作,当 ch 中有可以读取的数据时,则将值打印出来。没有的话则执行 default 语句,而这里 default 语句为空,所以继续下一次for语句,for{} 是一个死循环语句。当读取 15 次 ch 后,由于ch 会永远处于阻塞状态,所以会一直执行 default 条件,然后再执行 for 循环。此时这段逻辑基本演变成了一个空的 for{} 语句,所以会导致CPU占用100%。如果没有外层的 for{} 语句的话,这样写则没有任何问题的。

回到鸟窝的文章中,文章内为什么需要使用到default呢?文中提到,因为在批量处理的过程中,所以如果只是匹配Case的话,当channel中没有数据,并且当前batch 的数量还未达到设置的batchSize 的时候,程序就会一直等待,直到channel中有数据,或者channel 被关闭,这样会导致消费者饥饿。所以要避免这种情况时,应当使用default来处理这种情况的发生。

scss 复制代码
        default:
            if len(batch) > 0 {
                fn(batch)
                batch = make([]T, 0, batchSize) // reset
            }
        }

此时在第一个版本后,新增这段代码。

新增完这段代码之后,鸟叔提出,会导致CPU空转,结合我们上面的例子,就能明白,这段代码如果len(batch) ==0 的情况下,演变成一个空的for{}语句,导致无法释放P,CPU占用100%。

至此,我们明白了,为什么需要使用 default,以及 select default 会导致CPU占满的原因。

scss 复制代码
        case <-time.After(100 * time.Millisecond):
            if len(batch) > 0 {
                fn(batch)
                batch = make([]T, 0, batchSize) // reset
            }
        }

接着文章中提出使用<-time.After(100 * time.Millisecond) 代替default 来避免CPU空转,文章提到如果生产者生产数据的速度很快,而消费者处理数据的速度很慢,那么我们就会产生大量的Timer,这些 Timer 不能及时的被回收,可能导致大量的内存占用,而且如果有大量的 Timer,也会导致 Go 运行时处理 Timer 的性能。

为什么这里会产生大量的Timer?

解答:我的理解是两种情况:

  1. 进入到case时,其实每个对象都已经被生成了,所以都会创建出Timer这个对象。又由于消费者处理函数很慢,无法释放这个对象
  2. 当生成者的数据,每一次的间隔大于100 * time.Millisecond, 就会频繁的进入到 case <-time.After(100 * time.Millisecond) , 又由于消费者处理函数很慢,无法释放这个对象

如文中所说,time.After既带来了性能的问题,还可能导致它在休眠的时候不能及时读取 channel 中的数据,导致业务时延增加。

接下来请看最终版本。

go 复制代码
    default:
       if len(batch) > 0 { // partial
        fn(batch)
        batch = make([]T, 0, batchSize) // reset
       } else { // empty
        // wait for more
        select {
        case <-ctx.Done():
         if len(batch) > 0 {
          fn(batch)
         }
         return
        case v, ok := <-ch:
         if !ok {
          return
         }
    
         batch = append(batch, v)
        }

这个版本:

  1. 不会产生饥饿消费

  2. 没有走到的case ,进入default 分支,在该分支中,如果队列为空,则当前goroutine会发生 chan IO 阻塞, Go调度器根据GPM的调度关系,会将当前执行关系中的G切换出去,再从LRQ队列中取一个新的G,重新组成一个GPM继续执行,以实现合理利用计算机资源,提高GO的高并发性能

总结

  1. select语句只能用于信道的读写操作
  2. select中的case条件(非阻塞)是并发执行的,select会选择先操作成功的那个case条件去执行,如果多个同时返回,则随机选择一个执行,此时将无法保证执行顺序。对于阻塞的case语句会直到其中有信道可以操作,如果有多个信道可操作,会随机选择其中一个 case 执行
  3. 对于case条件语句中,如果存在信道值为nil的读写操作,则该分支将被忽略,可以理解为从select语句中删除了这个case语句
  4. 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的case,则执行这个超时case。如果此段时间内出现了可操作的case,则直接执行这个case。一般用超时语句代替了default语句
  5. 对于空的select{},会引起死锁
  6. 对于for中的select{}, 也有可能会引起cpu占用过高的问题
相关推荐
骆晨学长30 分钟前
基于springboot的智慧社区微信小程序
java·数据库·spring boot·后端·微信小程序·小程序
AskHarries35 分钟前
利用反射实现动态代理
java·后端·reflect
Flying_Fish_roe1 小时前
Spring Boot-Session管理问题
java·spring boot·后端
hai405872 小时前
Spring Boot中的响应与分层解耦架构
spring boot·后端·架构
Adolf_19933 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥3 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼3 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺3 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
新知图书4 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
wn5315 小时前
【Go - 类型断言】
服务器·开发语言·后端·golang