文章目录
并发队列
并发阻塞队列与channel对比
并发队列 | channel | |
---|---|---|
并发控制 | 加锁 | 天然支持 |
性能 | 锁竞争可能影响性能 | 无锁设计、性能不错,适合高并发场景 |
阻塞机制 | 需要自行控制等待机制,如使用sync.Cond | 天然支持 |
代码灵活性 | 支持复杂管理,比如: 1. 遍历,如sql.DB freeConn处理过期连接 2. 下标访问,如sql.DB连接健康检查——随机访问 3. 优先级、权重排序 4. 查看队首、队尾元素,判断出队时机 | FIFO,相较来说,支持简单操作 |
需求分析
功能性需求
- 线程安全 锁
- 队列为空时阻塞消费者or返回错误;队列满时阻塞生产者or返回错误;
- 阻塞超时阻塞
- 扩容问题
- 考虑大容量对GC的影响
- 元素优先级、权重
- 持久化
- 应用关闭时持久化数据,重启后恢复
非功能性需求
-
公平性与效率
- 是不是先到先得,尤其阻塞情况下
- channel不是公平的
- 效率:占着资源刚来的更容易抢到锁,因为资源分配有消耗
- 饥饿模式:队列等待时间太长进入饥饿模式后容易抢到锁
-
性能
-
ringBuffer替代[]*T
好处:b.queue = b.queue[1:]会引起内存频繁分配,ringBuffer复用固定资源
-
链表替代ringBuffer
好处: ringBuffer使用固定内存,可能实际不需要,链表支持动态连接
ringBuffer比链表的优势:支持复杂操作,如随机访问
-
接口设计
type Queue[T any] interface {
Enqueue(ctx context.Context, t *T) error
Dequeue(ctx context.Context) (*T, error)
}
实现方案一:切片实现
要点
-
并发控制:加锁
-
队列使用ringBuffer
-
使用cond进行超时阻塞控制
-
由于sync.cond没有超时控制,所以自行设计cond
-
等待时使用for,而不是if // 广播唤醒多个,但只有一个抢到了锁,其他需要重新等待枪锁
for b.queue.IsFull() { err := b.notFull.WaitV2(ctx) if err != nil { return err } }
-
源码
gitee: https://gitee.com/luyue_zhang/channel/blob/master/queue/concurrency_queue/slice_queue.go
实现方案二:链表实现
要点
- 并发控制:无锁实现,自旋(for循环)+ CAS
- 链表在高并发场景下无法提供准确的isFull、isEmpty、isLen值
源码
gitee: https://gitee.com/luyue_zhang/channel/blob/master/queue/concurrency_queue/link_queue.go
加锁实现与CAS+自旋对比
加锁 | CAS+自旋 |
---|---|
抢锁阻塞住时,不会有超高CPU消耗,但由于等待锁时间较长,时差较大 | 抢锁时会一直占用Processor,资源消耗高; 锁竞争越强,效果越差,CPU消耗越多; ringBuffer并发队列不能使用自旋+CAS,可能兜了一圈又回来了 |
优先级队列
要点
- 入队时需要排序
- 优先级队列引入排序,不适合使用ringBuffer、链表,使用slice
源码
gitee: https://gitee.com/luyue_zhang/channel/tree/master/queue/priority_queue
相关库
container/heap
延迟队列
要点
- 延迟队列是按照时间排序的优先级队列
- 元素出队逻辑:
- 队首元素Delay() <=0 直接出队
- 队首元素Delay() > 0,阻塞等待:
- 等待过程中超时返回
- 等待时间到达,则重新循环一次。因为释放了锁,可能元素已经被人取走了,所以需要重新循环判断一下;如果合法,下次将会从!t.Delay().After(now)出去
- 收到信号:新入队元素Delay() < 当前元素的Delay(),则再次循环。// 比起简单重置计时器更安全、可靠;因为等待过程中,元素可能已经发生变动
源码
gitee: https://gitee.com/luyue_zhang/channel/tree/master/queue/delay_queue