无限缓存的Channel
在Golang当中,Channel分为有缓冲的和无缓冲的,无缓冲的Channel可以用于同步,对于有缓冲的Channel容量在一开始的时候就已经创建好,后续无法改变,在一些特殊的场景下不太适用
- e.g.大量数据进行请求(并发)
- 引发本文实现UnlimitChannel(无限缓存)的想法
具体的业务场景需求
- 车端数据经过算法同学处理之后需要进行质检或者进行视频生成
- 一次性可能大量数据进行发送
- 质检之后根据请求参数对数据有不同的处理
- 上传到OSS或者TOS
- 调用其他平台接口
- 保存到本地
- 使用Channel进行通信,设置Channel的容量是有限制的(Channel容量不能设置为很大,这对内存的负担会比较大),请求的速度远远大于处理的速度->造成写阻塞->响应时间变得很长
能不能设计出一种无限缓冲的Channel?
设计要求
需要满足的条件
- 写入不会阻塞:因为大量请求的时候,写入会大于处理的速度
- 无数据的时候需要阻塞读:如果没有数据的时候,需要阻塞处理逻辑
- 读写都是通过Channel:不会暴露内部的缓存,外部看起来与普通Channel没有区别
- 关闭的Channel,数据依旧可读:与普通Channel保存一致
- 无限制的缓冲(UnboundedChan):最基本的要求
具体设计
有些同学好奇无限缓存的Channel是不是真的有无限的缓存?
- 答案:肯定不是无限的缓存
- 外部抽象来看是无限缓存,其实内部实现是使用一个中间层的数据结构暂时存储数据
中间层的设计
- 使用Golang标准库的链表库
"container/list"来作为中间缓存层
//xiaodai
type UnlimitChan struct {
In chan interface{}
Out chan interface{}
Name string
queue *Queue
}
- 以上是UnlimitChan
- 使用一个In(写入Channel),Out(读Channel),两者都设置为有缓存的Channel
- 使用Name来区分多个无限Channel
具体的中间层设计如下
//xiaodai
type Queue struct {
data *list.List
mutex sync.RWMutex
}
有同学就好奇了,为什么Queue还要加一个RWMutex?
请看"container/list"源码
// Remove removes e from l if e is an element of list l.
// It returns the element value e.Value.
// The element must not be nil.
func (l *List) Remove(e *Element) any {
if e.list == l {
// if e.list == l, l must have been initialized when e was inserted
// in l or l == nil (e is a zero Element) and l.remove will crash
l.remove(e)
}
return e.Value
}
- 在这里并没有对是否为nil进行判断,当e为nil的时候会造成程序崩溃
- 所以加上读写锁保证线程安全
Process设计
//in is closed
value, ok := <-ch.In
if !ok {
drain()
return
}
- 首先如果In已经关闭的话,需要drain未读的数据
drain := func() {
for ch.queue.Size() != 0 {
ch.Out <- ch.queue.Top()
ch.queue.Pop()
}
}
- 如果Out没有满的话,直接不走中间层,将数据传入Out当中
//out is not full
select {
case ch.Out <- value:
continue
default:
}
- 否则传入中间层,然后不断从中间层和In中取数据取数据
//out is full
ch.queue.Push(value)
//process cache
for ch.queue.Size() != 0 {
select {
case val, ok := <-ch.In:
if !ok {
drain()
return
}
ch.queue.Push(val)
case ch.Out <- ch.queue.Top():
ch.queue.Pop()
}
}
写测试验证此Channel是否达到预期效果
func TestMakeNewUnlimitChan(t *testing.T) {
ch := NewUnlimitChan(100, "test")
for i := 1; i < 200; i++ {
ch.In <- int64(i)
}
var count int64
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for v := range ch.Out {
fmt.Println(v.(int64))
count += v.(int64)
}
fmt.Println("read completed")
}()
for i := 200; i <= 1000; i++ {
ch.In <- int64(i)
}
close(ch.In)
wg.Wait()
if count != 500500 {
t.Fatalf("expected 500500 but got %d", count)
}
}
- 结果如下,验证通过~~~