热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

linux进阶50——无锁CAS

1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰
1. 概念

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

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

2. 实现原理

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

实现方式1:

//输入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;
}

实现方式2:

//输入一个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值让调用者知道是否更新成功。

3. 应用层实现

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

3.1 gcc/g++中的CAS

对于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, ...);

3.2 windows下的CAS

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

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

3.3 c++中的CAS

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

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

4. 无锁队列的实现

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

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

4.1 代码实现

queue_cas.h

//queue_cas.h
#include template
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
Queue::Queue()
{_head = _tail =new _qNode();
}template
Queue::~Queue()
{while(_head != nullptr){struct _qNode *tempNode = _head;_head = _head->_next;delete tempNode;}
}template
void Queue::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
bool Queue::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
void Queue::show()
{struct _qNode* tempNode &#61; _head->_next;if(tempNode &#61;&#61; nullptr){std::cout <<"Empty" <_elem <<" ";tempNode &#61; tempNode->_next;}std::cout <}

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

 

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

push()如下&#xff1a;

  • 假设线程T1和T2都执行push()函数&#xff0c;当线程T1先执行do-while中的CAS操作然后发现其尾节点后为空&#xff0c;于是就执行do-while中的CAS操作将尾节点_tail的_next指针赋值为newNode&#xff0c;然后退出do-while循环&#xff0c;调用第二个CAS操作将尾节点指针向后移动一位
  • 由于CAS是一个原子操作&#xff0c;所以即使同时T2线程了也调用了do-while中的CAS操作&#xff0c;但是其判断p->_next不为空&#xff0c;因为T1线程已经将尾节点向后移动了&#xff0c;所以其只能继续执行do&#xff0c;将p向后移动&#xff0c;重新移动到尾节点继续重新判断&#xff0c;直到成功为止....
  • 为什么push()函数的最后一个CAS操作不需要判断是否执行成功&#xff0c;因为&#xff1a;

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

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

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


  • do作用域中为什么要使用while将p指针向后移动&#xff1a;

假设T1线程在调用第二个CAS操作更新_tail指针之前&#xff0c;T1线程停掉或者挂掉了&#xff0c;那么其它线程就会进入死循环

template
void Queue::push(ElemType elem)
{//创建一个新的节点struct _qNode *newNode &#61; new struct _qNode(elem);struct _qNode *p &#61; _tail;struct _qNode *oldp &#61; _tail;do{//不断的向后指&#xff0c;直到直到尾节点为止while(p->_next !&#61; nullptr)p &#61; p->_next;} while(__sync_bool_compare_and_swap(&p->_next, nullptr, newNode) !&#61; true); //如果p没有移动到真正的尾节点上&#xff0c;那么继续执行do//当CAS函数执行成功之后&#xff0c;那么执行这个CAS函数&#xff0c;将尾节点指针向后移动1位__sync_bool_compare_and_swap(&_tail, oldp, newNode);
}

pop()如下&#xff1a;

  • 原理与push()同理&#xff0c;假设线程T1和线程T2都执行pop()操作&#xff0c;假设T1先执行CAS操作将_head向后移动了一位&#xff0c;并且删除了原先的头指针
  • 那么当T2再执行时发现T1更新过后的_head指针(移动了)与一开始获取的头指针p不相等了&#xff0c;那么就继续执行do作用域重新获取头指针&#xff0c;然后重新进行CAS操作

template
bool Queue::pop()
{struct _qNode *p;do {//获取_head指针p &#61; _head;if(p->_next &#61;&#61; nullptr)return false;} while(__sync_bool_compare_and_swap(&_head, p , p->_next) !&#61; true); //判断头结点是否被移动过&#xff0c;如果移动过那么就执行do内部重新获取_head指针//删除头指针delete p;return true;
}

4.2 测试代码

//queue_cas_test.cpp
#include "queue_cas.h"int main()
{Queue 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();
}

编译运行&#xff1a;

[root&#64;192 CAS]# g&#43;&#43; queue_cas.cpp -o queue_cas
[root&#64;192 CAS]# ls
queue_cas queue_cas.cpp queue_cas.h
[root&#64;192 CAS]# ./queue_cas
1 2 3
2 3
3
Empty
1
1 2
[root&#64;192 CAS]#

5. 无锁队列性能测试

5.1 stl库中的queue

#include
#include
#include
#include
#include using namespace std;#define FOR_LOOP_NUM 10000000 //队列push和pop操作函数中for循环的次数static std::queue _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 &#61; 0; i }void *queue_pop(void *arg)
{while(true){_mutex.lock();if(_queue.size() > 0){_queue.pop();pop_count&#43;&#43;;}_mutex.unlock();if(pop_count >&#61; FOR_LOOP_NUM)break;}return NULL;
}void test_queue(thread_func_t push_func, thread_func_t pop_func, void *arg)
{clock_t start &#61; clock();pthread_t push_tid;if(pthread_create(&push_tid, NULL, push_func, arg) !&#61; 0){perror("pthread_create");}pthread_t pop_tid;if(pthread_create(&pop_tid, NULL, pop_func, arg) !&#61; 0){perror("pthread_create");}pthread_join(push_tid, NULL);pthread_join(pop_tid, NULL);clock_t end &#61; clock();printf("spend clock: %ld\n", (end - start) / CLOCKS_PER_SEC);
}int main()
{push_count &#61; 0;pop_count &#61; 0;test_queue(queue_push, queue_pop, NULL);printf("push_count:%d, pop_count:%d\n", push_count, pop_count);return 0;
}

编译运行&#xff1a;

[root&#64;192 queue_stl]# g&#43;&#43; -lpthread queue_stl.cpp -o queue_stl
[root&#64;192 queue_stl]# ls
queue_stl queue_stl.cpp
[root&#64;192 queue_stl]# ./queue_stl
spend clock: 15
push_count:10000000, pop_count:10000000
[root&#64;192 queue_stl]# ./queue_stl
spend clock: 16
push_count:10000000, pop_count:10000000
[root&#64;192 queue_stl]# ./queue_stl
spend clock: 15
push_count:10000000, pop_count:10000000
[root&#64;192 queue_stl]# ./queue_stl
spend clock: 14
push_count:10000000, pop_count:10000000
[root&#64;192 queue_stl]# ./queue_stl
spend clock: 17
push_count:10000000, pop_count:10000000
[root&#64;192 queue_stl]#

5.2 无锁队列

#include
#include
#include
#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 _queue;typedef void *(*thread_func_t)(void *arg);void *queue_push(void *arg)
{for(int i &#61; 0; i }void *queue_pop(void *arg)
{while(true){_queue.pop();pop_count&#43;&#43;;if(pop_count >&#61; FOR_LOOP_NUM)break;}return NULL;
}void test_queue(thread_func_t push_func, thread_func_t pop_func, void *arg)
{clock_t start &#61; clock();pthread_t push_tid;if(pthread_create(&push_tid, NULL, push_func, arg) !&#61; 0){perror("pthread_create");}pthread_t pop_tid;if(pthread_create(&pop_tid, NULL, pop_func, arg) !&#61; 0){perror("pthread_create");}pthread_join(push_tid, NULL);pthread_join(pop_tid, NULL);clock_t end &#61; clock();printf("spend clock: %ld\n", (end - start) / CLOCKS_PER_SEC);
}int main()
{push_count &#61; 0;pop_count &#61; 0;test_queue(queue_push, queue_pop, NULL);printf("push_count:%d, pop_count:%d\n", push_count, pop_count);return 0;
}

编译运行&#xff1a;

[root&#64;192 CAS]# g&#43;&#43; -lpthread queue_cas.cpp -o queue_cas
[root&#64;192 CAS]# ls
queue_cas queue_cas.cpp queue_cas.h queue_stl
[root&#64;192 CAS]# ./queue_cas
spend clock: 1
push_count:10000000, pop_count:10000000
[root&#64;192 CAS]# ./queue_cas
spend clock: 2
push_count:10000000, pop_count:10000000
[root&#64;192 CAS]# ./queue_cas
spend clock: 1
push_count:10000000, pop_count:10000000
[root&#64;192 CAS]# ./queue_cas
spend clock: 1
push_count:10000000, pop_count:10000000
[root&#64;192 CAS]# ./queue_cas
spend clock: 1
push_count:10000000, pop_count:10000000
[root&#64;192 CAS]# ./queue_cas
spend clock: 2
push_count:10000000, pop_count:10000000
[root&#64;192 CAS]#

对比发现&#xff0c;无锁队列的效率更高。


推荐阅读
  • 线程漫谈——线程基础
    本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。进程与线程理解线程是至关重要的,每个进程至少有一个线程,进程是线程的容器,线程才是真正的执行体,线程必 ... [详细]
  • 第七课主要内容:多进程多线程FIFO,LIFO,优先队列线程局部变量进程与线程的选择线程池异步IO概念及twisted案例股票数据抓取 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 作者一直强调的一个概念叫做oneloopperthread,撇开多线程不谈,本篇博文将学习,怎么将传统的IO复用pollepoll封装到C++类中。1.IO复用复习使用p ... [详细]
  • Linux_多线程(进程与线程的联系_pthread库_线程创建_线程等待_线程正常终止_线程取消_线程分离_pthread_t与LWP)
    Linux_多线程(进程与线程的联系_pthread库_线程创建_线程等待_线程正常终止_线程取消_线程分离_pthread_t与LWP)-文章目录1.线程的定义,进程和线程 ... [详细]
  • 采用CreateThread()创建多线程程序
    本位转自:http:blog.csdn.netcbnotesarticledetails8277180在window环境下,Win32提供了一系列的AP ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 先看官方文档TheJavaTutorialshavebeenwrittenforJDK8.Examplesandpracticesdescribedinthispagedontta ... [详细]
  • 本文介绍了使用哈夫曼树实现文件压缩和解压的方法。首先对数据结构课程设计中的代码进行了分析,包括使用时间调用、常量定义和统计文件中各个字符时相关的结构体。然后讨论了哈夫曼树的实现原理和算法。最后介绍了文件压缩和解压的具体步骤,包括字符统计、构建哈夫曼树、生成编码表、编码和解码过程。通过实例演示了文件压缩和解压的效果。本文的内容对于理解哈夫曼树的实现原理和应用具有一定的参考价值。 ... [详细]
  • Linux 中使用 clone 函数来创建线程
    2019独角兽企业重金招聘Python工程师标准Linux上创建线程一般使用的是pthread库实际上libc也给我们提供了创建线程的函数那就是cloneintclone(i ... [详细]
  • 主线:设计窗口类注册窗口类产生窗口显示窗口更新窗口消息循环(将消息路由到窗口中去处理)。APPMODUL.CPP源文件被编译链接进入项目,从APPMOD ... [详细]
  • 不知道你是否还记得之前在进程中的信号处理时,提到过阻塞信号集与未决信号集的概念,如果你已经忘记了,请参考《阻塞信号与未决信号》一文回忆一下 ... [详细]
  • 线程概念在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列指令序列”;一切进程至少都有一个执行线程;  进程  VS. 线程  ... [详细]
  • 主要用的线程函数:1.创建线程:12intpthread_create(pthread_t*thread,constpthread_attr_ ... [详细]
author-avatar
多米音乐_35794462
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有