无锁CAS--备份,有问题

一、引言

锁是解决并发问题的万能钥匙,可是并发问题只有锁能解决吗?当然不是,CAS也可以解决并发问题

二、什么是CAS

比较并交换(compare and swap,CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某⼀数据时由于执行顺序不确定性以及中断的不可预知性产⽣的数据不一致问题

有了CAS,我们就可以用它来实现各种无锁(lock free)的数据结构

实现原理

该操作通过将内存中的值与指定数据进行比较,当数值⼀样时将内存中的数据替换为新的值

下面是两种int类型操作的CAS伪代码形式:

//输入reg的地址,判断reg的值与oldval是否相等
//如果相等,那么就将newval赋值给reg;否则reg保持不变
//最终将reg原先的值返回回去int compare_and_swap(int *reg, int oldval, int newval)
{int old_ref_val = *reg;if(old_reg_val == oldval)*reg = newval;return old_reg_val;
}

//输入一个pAddr的地址,在函数内部判断其的值是否与期望值nExpected相等
//如果相等那么就将pAddr的值改为nNew并同时返回true;否则就返回false,什么都不做bool compare_and_swap(int *pAddr, int nExpected, int nNew)
{if(*pAddr == nExpected){*pAddr = nNew;return true;}elsereturn false;
}

在上面的两种实现中第二种形式更好,因为它返回bool值让调用者知道是否更新成功

三、CAS的应用层实现

因为CAS是原子操作,所以在各种库的原子库中都有对应的CAS实现方式

gcc/g++中的CAS
CAS in gcc/g++

对于gcc、g++编译器来讲,其原子操作中包含下面两个函数,是专门用来做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
Windows本地CAS

在Windows下,你可以使用下面的Windows API来完成CAS:

InterlockedCompareExchange ( __inout LONG volatile  *Target,__in LONG Exchange,__in LONG Comperand);

C++中的CAS
C++发表评论

C++11标准库引入了原子操作,包含在<atomic>头文件中,下面是专门用于CAS操作的接口

template< class T >
bool atomic_compare_exchange_weak( std::atomic<T>* obj,T* expected, T desired );
template< class T >
bool atomic_compare_exchange_weak( volatile std::atomic<T>* obj,T* expected, T desired );

四、无锁队列的实现

此处我们只考虑队列出队列和进队列的并发问题:

出队列:出队列时,要保证只有一个线程在对头结点进行出队列的操作,否则就会发生错乱

入队列:入队列时,也一样,保证只有一个线程在对尾节点进行入队列的操作,否则就会发生错乱

无锁队列代码实现

//queue_cas.h
#include <iostream>template<typename ElemType>
class Queue
{
public:Queue();                  //构造函数~Queue();                 //析构函数
public:void push(ElemType elem); //入队列bool pop();               //出队列void show();              //打印队列的内容
private:struct _qNode             //队列节点{_qNode(): _next(nullptr) { } _qNode(ElemType elem): _elem(elem), _next(nullptr) { } ElemType       _elem;struct _qNode *_next;};
private:struct _qNode *_head;    //头结点struct _qNode *_tail;    //尾节点
};template<typename ElemType>
Queue<ElemType>::Queue()
{_head = _tail =new _qNode();
}template<typename ElemType>
Queue<ElemType>::~Queue()
{while(_head != nullptr){struct _qNode *tempNode = _head;_head = _head->_next;delete tempNode;}
}template<typename ElemType>
void Queue<ElemType>::push(ElemType elem)
{//创建一个新的节点struct _qNode *newNode = new struct _qNode(elem);struct _qNode *p = _tail;struct _qNode *oldp = _tail;do{while(p->_next != nullptr)p = p->_next;} while(__sync_bool_compare_and_swap(&_tail->_next, nullptr, newNode) != true);__sync_bool_compare_and_swap(&_tail, oldp, newNode);
}template<typename ElemType>
bool Queue<ElemType>::pop()
{struct _qNode *p;do {p = _head;if(p->_next == nullptr)return false;} while(__sync_bool_compare_and_swap(&_head, p , p->_next) != true);delete p;return true;
}template<typename ElemType>
void Queue<ElemType>::show()
{struct _qNode* tempNode = _head->_next;if(tempNode == nullptr){std::cout << "Empty" <<std::endl;return;} while(tempNode != nullptr){std::cout << tempNode->_elem << " ";tempNode = tempNode->_next;}std::cout << std::endl;
}

上面为无锁队列的实现代码,我们假定此队列中头结点不存储数据(当做哨兵),尾节点存储数据

其使用到CAS的核心函数就是push()和pop()函数,在下面我们将_sync_bool_compare_and_swap()函数调用称之为CAS操作

push()如下:

假设线程T1和T2都执行push()函数,当线程T1先执行do-while中的CAS操作然后发现其尾节点后为空,于是就执行do-while中的CAS操作将尾节点_tail的_next指针赋值为newNode,然后退出do-while循环,调用第二个CAS操作将尾节点指针向后移动一位

由于CAS是一个原子操作,所以即使同时T2线程了也调用了do-while中的CAS操作,但是其判断p->_next不为空,因为T1线程已经将尾节点向后移动了,所以其只能继续执行do,将p向后移动,重新移动到尾节点继续重新判断,直到成功为止....

为什么push()函数的最后一个CAS操作不需要判断是否执行成功,因为:

1.如果有一个线程T1,它的while中的CAS如果成功的话,那么其它所有的随后线程的CAS都会失败,然后就会再循环

2.此时,如果T1线程还没有更新tail指针,其它的线程继续失败,因为tail->next不是NULL了

3.直到T1线程更新完tail指针,于是其它的线程中的某个线程就可以得到新的tail指针,继续往下走了

do作用域中为什么要使用while将p指针向后移动:

  • 假设T1线程在调用第二个CAS操作更新_tail指针之前,T1线程停掉或者挂掉了,那么其它线程就会进入死循环
template<typename ElemType>
void Queue<ElemType>::push(ElemType elem)
{//创建一个新的节点struct _qNode *newNode = new struct _qNode(elem);struct _qNode *p = _tail;struct _qNode *oldp = _tail;do{//不断的向后指,直到直到尾节点为止while(p->_next != nullptr)p = p->_next;} while(__sync_bool_compare_and_swap(&p->_next, nullptr, newNode) != true); //如果p没有移动到真正的尾节点上,那么继续执行do//当CAS函数执行成功之后,那么执行这个CAS函数,将尾节点指针向后移动1位__sync_bool_compare_and_swap(&_tail, oldp, newNode);
}

pop()如下:

  • 原理与push()同理,假设线程T1和线程T2都执行pop()操作,假设T1先执行CAS操作将_head向后移动了一位,并且删除了原先的头指针
  • 那么当T2再执行时发现T1更新过后的_head指针(移动了)与一开始获取的头指针p不相等了,那么就继续执行do作用域重新获取头指针,然后重新进行CAS操作
template<typename ElemType>
bool Queue<ElemType>::pop()
{struct _qNode *p;do {//获取_head指针p = _head;if(p->_next == nullptr)return false;} while(__sync_bool_compare_and_swap(&_head, p , p->_next) != true); //判断头结点是否被移动过,如果移动过那么就执行do内部重新获取_head指针//删除头指针delete p;return true;}

测试代码

//queue_cas_test.cpp
#include "queue_cas.h"int main()
{Queue<int> queue;queue.push(1);queue.push(2);queue.push(3);queue.show();queue.pop();queue.show();queue.pop();queue.show();queue.pop();queue.show();queue.push(1);queue.show();queue.push(2);queue.show();
}

我们编写下面的程序测试一下无锁队列的各种操作是否有误,结果显示无误

五、无锁队列性能测试

下面我们将上面的无锁队列与C++ STL库中的queue进行对比,查看一下性能

queue_stl.cpp

#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include <queue>
#include <mutex>using namespace std;#define FOR_LOOP_NUM 10000000  //队列push和pop操作函数中for循环的次数static std::queue<int> _queue; //队列
static std::mutex      _mutex; //队列操作要用到的互斥锁static int push_count;         //队列总共push的次数
static int pop_count;          //队列总共pop的次数typedef void *(*thread_func_t)(void *arg);void *queue_push(void *arg)
{for(int i = 0; i < FOR_LOOP_NUM; ++i){_mutex.lock();_queue.push(i);push_count++;_mutex.unlock();}return NULL;
}void *queue_pop(void *arg)
{while(true){_mutex.lock();if(_queue.size() > 0){_queue.pop();pop_count++;}_mutex.unlock();if(pop_count >= FOR_LOOP_NUM)break;}return NULL;
}void test_queue(thread_func_t push_func, thread_func_t pop_func, void *arg)
{clock_t start = clock();pthread_t push_tid;if(pthread_create(&push_tid, NULL, push_func, arg) != 0){perror("pthread_create");}pthread_t pop_tid;if(pthread_create(&pop_tid, NULL, pop_func, arg) != 0){perror("pthread_create");}pthread_join(push_tid, NULL);pthread_join(pop_tid, NULL);clock_t end = clock();printf("spend clock: %ld\n", (end - start) / CLOCKS_PER_SEC);
}int main()
{push_count = 0;pop_count = 0;test_queue(queue_push, queue_pop, NULL);printf("push_count:%d, pop_count:%d\n", push_count, pop_count);return 0;
}

其结果显示,执行10000000万次push和 10000000万次pop操作大概要1秒多的时间

queue_cas.cpp

#include <stdio.h>
#include <time.h>
#include <pthread.h>
#include "queue_cas.h"using namespace std;#define FOR_LOOP_NUM 10000000  //队列push和pop操作函数中for循环的次数static int push_count;         //队列总共push的次数
static int pop_count;          //队列总共pop的次数static Queue<int> _queue;typedef void *(*thread_func_t)(void *arg);void *queue_push(void *arg)
{for(int i = 0; i < FOR_LOOP_NUM; ++i){_queue.push(i);push_count++;}return NULL;
}void *queue_pop(void *arg)
{while(true){_queue.pop();pop_count++;if(pop_count >= FOR_LOOP_NUM)break;}return NULL;
}void test_queue(thread_func_t push_func, thread_func_t pop_func, void *arg)
{clock_t start = clock();pthread_t push_tid;if(pthread_create(&push_tid, NULL, push_func, arg) != 0){perror("pthread_create");}pthread_t pop_tid;if(pthread_create(&pop_tid, NULL, pop_func, arg) != 0){perror("pthread_create");}pthread_join(push_tid, NULL);pthread_join(pop_tid, NULL);clock_t end = clock();printf("spend clock: %ld\n", (end - start) / CLOCKS_PER_SEC);
}int main()
{push_count = 0;pop_count = 0;test_queue(queue_push, queue_pop, NULL);printf("push_count:%d, pop_count:%d\n", push_count, pop_count);return 0;
}

其结果显示,执行10000000万次push和 10000000万次pop操作大概在1秒之内,没有超过1秒中

因此,无锁队列比使用mutex的效率要高一些

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/456206.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

活锁方案与自旋锁

问题 如何设置获取互斥量时的等待时间&#xff1f; 如果等待超时&#xff0c;如何避免死锁&#xff1f; 避免死锁 -- 设置等待超时 解决方案&#xff1a; 1、尝试获取第 1 个互斥量&#xff1a; 若成功&#xff0c;则转 2 执行&#xff1b;若失败&#xff0c;则等待&#x…

部署tomcat

1 idea打包完&#xff0c;找到target目录下的网站 我的叫做 test.webSYS 2 linux 安装 tomcat 后 修改conf下的server.xml中的Connector的port <Connector port"9001" protocol"HTTP/1.1"connectionTimeout"20000"redirectPort"8443…

深度学习缝模块怎么描述创新点?(附写作模板+涨点论文)

深度学习缝了别的模块怎么描述创新点、怎么讲故事写成一篇优质论文&#xff1f; 简单框架&#xff1a;描述自己这个领域&#xff0c;该领域出现了什么问题&#xff0c;你用了什么方法解决&#xff0c;你的方法有了多大的性能提升。 其中&#xff0c;重点讲清楚这两点&#xf…

关于node.js奇数版本不稳定 将11.x.x升级至16.x.x不成功的一系列问题(一)

据说vue2用16稳定一些 vue3用18好一点&#xff08;但之前我vue3用的16.18.1也可以&#xff09; 为维护之前的老项目 先搞定node版本切换 下载nvm node版本管理工具 https://github.com/coreybutler/nvm-windows/releases 用这个nvm-setup.zip安装包 安之前最好先将之前的nod…

问题:下列关于海关统计项目的表述,正确的有:A.进出境货物的统计重量和数量应以报关单位申报的重量和数 #笔记#职场发展#媒体

问题&#xff1a;下列关于海关统计项目的表述&#xff0c;正确的有&#xff1a;A&#xff0e;进出境货物的统计重量和数量应以报关单位申报的重量和数 下列关于海关统计项目的表述&#xff0c;正确的有&#xff1a; A&#xff0e;进出境货物的统计重量和数量应以报关单位申报的…

护眼台灯对眼睛有伤害吗?推荐适合考研使用的台灯

随着科技的进步&#xff0c;台灯的外观和造型都在不断发展&#xff0c;逐渐台灯的功能也多元化&#xff0c;各式各样的台灯都有&#xff0c;其中最受广大家长、学生党、办公族喜爱的就是护眼台灯。不过也有些小伙伴问&#xff0c;护眼台灯对眼睛有伤害吗&#xff1f; 其实正确的…

Linux虚拟文件系统(VFS)

虚拟地址空间通常是与进程密切相关的概念&#xff0c;而不是文件系统。虚拟地址空间是为了提供进程对内存的抽象和隔离而设计的。 文件系统不使用页表&#xff0c;直接使用物理地址。 虚拟文件系统是linux内核的一个核心子系统。、 虚拟文件系统的目的&#xff1a;通过一个抽…

2024年2月CCF-全国精英算法大赛题目

第一次参加这种比赛&#xff0c;虽然是c类赛事&#xff0c;但是是ccf主办的&#xff0c;难度还是有点的&#xff0c;主要是前面签到题主要是思想&#xff0c;后面的题目难度太高&#xff0c;身为力扣只刷了一百多道题目的我解决不了&#xff0c;这几道我只做了B,C题,E题超时了&…

Linux 命令基础

Shell概述 Linux操作系统的Shell作为操作系统的外壳&#xff0c;为用户提供使用操作系统的接口。它是命令语言、命令解释程序及程序设计语言的统称。 Shell是用户和Linux内核之间的接口程序&#xff0c;如果把硬件想象成一个球体的中心&#xff0c;内核围绕在硬件的外层管理着…

每日一练:LeeCode-112、路径总和【二叉树+DFS+回溯】

本文是力扣LeeCode-112、路径总和 学习与理解过程&#xff0c;本文仅做学习之用&#xff0c;对本题感兴趣的小伙伴可以出门左拐LeeCode。 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有…

C语言第十九弹---指针(三)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】 指针 1、数组名的理解 2、使用指针访问数组 3、⼀维数组传参的本质 4、冒泡排序 5、二级指针 6、指针数组 7、指针数组模拟二维数组 总结 1、数组名的理解…

基于SSM的协同过滤技术的网上书城(有报告)。Javaee项目。ssm项目。

演示视频&#xff1a; 基于SSM的协同过滤技术的网上书城&#xff08;有报告&#xff09;。Javaee项目。ssm项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&#xff0c;通过Sp…