背景
在进行实际生产多线程开发的时候通常不会直接使用使用锁机制来操作线程间传递的数据,特别是对效率要求很高的场景中。最典型的就是音视频项目或者网络项目。这里先拿网络传输场景举例,
从这篇开始就开始详细介绍无锁队列的原理以及C++的实现。
先看下图:
上面场景中网络信息接收线程会从网络API中源源不断的接收到通过网络发过来的信息并且将其还原成有意义的数据。这些数据会立即传递到信息处理线程中进行业务处理,因为业务本身通常需要很大的计算量,如果这两个线程每次访问需要传递的数据都进行锁操作,则会降低并发的效率。因此引入了无锁队列。
原理实现
上面介绍了无锁队列所解决的问题以及引入的场景。具体到无锁队列的实现与普通队列的实现有下面几点需要注意的地方
- 使用场景符合生产者消费者模型
- 通常采用环形队列的方式实现
- 访问队列时需要使用CAS(Compare and Swap)操作来保证原子性
- 要考虑到所访问操作代码被编译器优化以及内存不一致性
下面针对以上四点来详细说明一下如何实现一个无锁队列。
生产者消费者模型
最常用的模型,该模型一共分三部分
- 生产者
- 交换区
- 消费者
并且彼此需要符合一下特点
- 生产者只生产数据并且只负责将数据写入到交换区中
- 消费者只负责从交换区中获取数据处理
- 生产者和消费者之间不会相互依赖,只保证对交换区正常操作
从上面三点也能推导出其余特点,例如如果交换区为空时消费者不会获取到新数据以及交换区满时无法再写入数据。
另外生产者和消费者也可以不唯一,可以同时有多个生产者和消费者。
只要符合以上特征就是一个生产消费者模型,并且适合采用无锁队列来进行实现传输数据,其带来的有点也很明显
- 解耦
- 并发
从上面生产消费者模型的描述中可以看出该模型中最重要的部分就是在于交换区的实现。而交换区实现就就采用环形队列。
环形队列
普通的队列非常简单,有队首有队尾,根据不同的用途可以采用简单或者复杂的实现,例如栈,双向队列等。如下面这个结构体就可以看作时最简单的一个元素为int的队列,
struct Queue
{
int arrQueueBuffer[MAX_PATH];
int* pCurrent;
};
arrQueueBuffer本身可以作为队列头,插入或者删除元素时只需要对比是否超出或者小于arrQueueBuffer的大小即可。这实际上也就是个栈结构。
而环形队列就是采用了两个指针,写指针和读指针。无论写指针和读指针在超过最大大小时就会回到缓冲区开头。从而将一个线性的数组或者链表在逻辑上形成了一个环状结构。如下图所示
下一篇将详细讨论环形队列的实现。