C++无锁队列

无锁队列

添加锁的局限性

在进行多线程开发的时候,为了保证不同线程数据的一致性,一般会使用mutex进行加锁。这一点属于基础。不过,使用锁的方式在高速并发程序的时候,会存在一些局限性,导致程序运行性能受损。
具体来说,如下:
1、Cache trashing
Cache 是高速缓存,由于CPU和RAM之间的主频不一致,CPU的性能大幅度高出了RAM的性能,导致了CPU读取、下写数据的频率远远超出RAM,会令CPU去等待RAM完成相关操作,从而使得CPU性能浪费。为了保证现代计算机系统发挥高性能,CPU和RAM之间的引入Cache,它可以进行更高速的数据交换(当然,价钱也会更贵,这也是无法大量装配的主要因素)。这就极大弥补了CPU和RAM之间性能不一致。
CPU在计算时,首先会访问Cache,如果Cache中没有相关数据,Cache会从RAM中加载,同时,CPU也会从RAM中读取一部分数据,并在Cache读取数据后,从Cache中读取一部分数据。
而由于线程在抢占工作时,需要回复context中的内容,这期间,由于各个线程context的不同,由于CPU可能会使用context数据,Cache会在线程恢复的时候,读取context数据,然而,Cache的容量是有限的,当Cache容量超出时,会删除掉最不常用的数据,加载新的数据。所以,频繁的线程抢占,可能会导致需要的数据被Cache丢掉,导致数据读取变慢。损失掉性能。
2、线程抢夺性能损失
线程阻塞和唤醒并不是没有代价的。线程阻塞会令操作系统终止当前线程的运行,并且保存context,等待锁资源被释放,才会进行context恢复,并继续运行下去。这其实会浪费相当的计算机资源。频繁的线程抢占,会使得计算机的大量资源都浪费在线程切换上,而不是去处理程序员关心的数据。
3、堆内存分配问题
程序在运行时,如果动态开辟、释放内存(堆上),(new、delete),为了保证内存分配的独占性,内存分配机制会阻塞所有与这个任务共享地址空间的其它任务(进程中的所有线程)。如果一个并发程序在运行时存在大量的堆内存使用,这必然会导致程序性能降低。

什么是原子操作

简单来说,原子操作就是CPU在处理事务的最基本单元。原子操作不会被中断抢夺执行优先级。
我们都知道,在进行多线程开发时,加锁是为了保证线程内,数据读取和下写时的独占。这是因为CPU读取寄存器和写入寄存器时,可能会由于线程切换,导致数据出错。但是,如果,一个线程在读取和写入数据的时候,是连续的,不会被其他线程打断,这就能保证线程数据操作的独占性。这样,就不需要进行加锁了。

无锁队列实现原理

CAS

无锁队列的实现就是基于上述将寄存器中数据的读取和写入连续进行,不会被中断的进行这一特性。这种特性的应用就是CAS。
该操作通过将内存中的值与指定数据进⾏⽐较,当数值⼀样时将内存中的数据替换为新的值。CAS是所有CPU指令都支持CAS的原子操作(X86中CMPXCHG汇编指令),用于实现实现各种无锁(lock free)数据结构。
CAS用于检查一个内存位置是否包含预期值,如果包含,则把新值复赋值到内存位置。成功返回true,失败返回false。

bool compare_and_swap ( int *memory_location, int expected_value, int new_value)
{
   
    if (*memory_location == expected_value)
    {
   
        *memory_location = new_value;
        return true;
    }
    return false;
}

GCC对CAS的支持

bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...);
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...);

windows对CAS的支持

LONG InterlockedCompareExchange(
  LONG volatile *Destination,
  LONG          ExChange,
  LONG          Comperand
);

队列

针对队列,依据场景的不同,分成四种类型
1、single builder —— single consumer
在这里插入图片描述
这种情况,只有一个builder线程去向队列中注入任务,一个consumer线程从队列中获取任务,并且执行该任务。

2、multi builer —— multi consumer
在这里插入图片描述
这种情况,有多个builder线程向队列中注入任务,只有一个consumer获取队列中的任务,并且执行该任务。
3、single builder —— multi consumer
在这里插入图片描述
4、multi builder —— multi consumer
在这里插入图片描述
这四种队列,每一个builder和consumer都是进程内的不同线程,为了保证各个builder和consumer线程都能够高效的进行运算,就需要task Queue作为无锁队列,保证数据的独占性。

无锁队列的实现

RingBuffer

RingBuffer(环形存储器),这是实现数据队列的一种方式,可以防止栈溢出。
builder和consumer各维护各的访问队列的index,称为rear和front。
builder负责将task添加到队列的末尾,并且修改rear的值(rear++),当到达队列末尾时(即队列长度到达 QueueMaxLen时),绕回到队列头部(rear = 0)。
consumer负责从队列的头部获取task数据,并且修改front的值(front ++),当到达队列末尾时,也绕回到头部。
在这里插入图片描述

开始队列为空的时候,rear和front都指向队列的头部,当有task进入时,先添加task,再将rear++,而当有task取出时,先取出task,再将front++。

single builder —— single consumer

针对这种情况,因为,队列只有一个线程push task, 一个线程pop task,即一个builder维护rear,一个consumer维护front。两个线程之间并没有共同需要维护的数据,所以也就不存在数据的竞争。这种情况下,就不需数据队列进行原子操作,就可以直接进行数据的写入。

其他情况

针对其他情况,就有必要引入原子操作,来保证队列中数据不会产生数据竞争。

CircleQueue实现

头文件
#pragma once
#include <atomic>
#include <windows.h>
enum  CircularQueueValueStatus : unsigned int
{
   
	STATUS_INVALID = 0,
	STATUS_LOCKED,
	STATUS_VALID,
};
template<class T>
class CircularQueue
{
   
public:
	CircularQueue();
	~CircularQueue();
public:
	bool init(unsigned int maxNum = 4096);
	
  • 4
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值