C++ 无锁队列 ABA <1>
来源:互联网 发布:jquery.easing.min.js 编辑:程序博客网 时间:2024/06/06 14:12
实验环境:vs2013 新建一个无stdafx.h预编译头的控制台程序,然后复制以下代码
1、链表实现无锁队列
2、数组实现无锁队列
1、链表
注意: Enqueue函数中有使用new分配内存,本人在windows下使用VS2013编译,这里的new是线程安全的?并且如果换成LINUX或者其它系统的其它编译器,则可能由于new库不支持多线程而导致new不是线程安全的。因此,移植到其它系统时可以把在new前后加锁或者把new动作放在Enqueue函数外面,函数参数加一个参数即可!!
#include <stdio.h>#include <process.h>#include <windows.h>#include <vector>#include <iostream>using namespace std;#define CAS(a,b,c) (InterlockedCompareExchangePointer((PVOID*)a,(PVOID)c,(PVOID)b) == b)struct Node{void *data;Node *next;};struct Queue{Node *head;Node *tail;};//初始化:最开始需要有一个空的节点void InitQueue(Queue *&queue){Node *node = new Node;node->next = nullptr;queue = new Queue;queue->head = queue->tail = node;}void UnInitQueue(Queue *queue){if(queue != nullptr){Node* node = queue->head;Node* next = nullptr;while(node != nullptr){next = node->next;delete node;node = next;}delete queue;}}void Enqueue(Queue *queue,void *data){Node *node, *tail, *next;node = new Node;node->data = data;node->next = nullptr;while (true){tail = queue->tail;next = tail->next;if (queue->tail != tail)continue;if (next != nullptr){CAS(&queue->tail, tail, next);continue;}if (CAS(&tail->next, nullptr, node))break;}CAS(&queue->tail, tail, node);}void* Dequeue(Queue *queue){Node *head, *tail, *next;void* data = nullptr;while (true){head = queue->head;tail = queue->tail;next = head->next;if (queue->head != head)continue;if (next == nullptr)return nullptr;if (head == tail){CAS(&queue->tail,tail,next);continue;}//多消费者,next内存可能已经释放,next->data程序可能崩溃,单消费者,则不可能存在已经释放的情况data = next->data;//存在ABA问题if (CAS(&queue->head, head, next))break;}delete head;return data;}/////////////////////////////////////测试代码///////////////////////////////////////////////////struct Test{int id; //线程索引int num; //每个线程生产元素个数Queue *queue;HANDLE event;int count;//每个消费者线程消费的个数};unsigned int __stdcall EnqueueThread(void *data){Test *test = (Test*)data;Queue *queue = test->queue;int num = test->num;int start = num * (test->id+1);int end = start - num;::SetEvent(test->event);while (start > end){int *a = new int;*a = start;Enqueue(queue, a);start--;}return 0;}unsigned int __stdcall DequeueThread(void *data){Test *test = (Test*)data;Queue *queue = test->queue;int id = test->id;int &count = test->count;::SetEvent(test->event);int null_num = 0,num = 0;while (true){int * ptr = (int*)Dequeue(queue);if (ptr == nullptr)null_num++;else{delete ptr;ptr = nullptr;num++;}if (null_num == 5000000) break;}Sleep(100);cout << "该消费者线程索引=" << id << ",退出时消费元素个数为 num=" << num << " null_num=" << null_num << endl;count = num;return 0;}void test(){Queue *queue = nullptr;InitQueue(queue);HANDLE hThreadEvent = ::CreateEvent(NULL, 0, 0, NULL);Test test;test.queue = queue;test.event = hThreadEvent;test.num = 10000; //单个生产者线程生产的元素数量test.count = 0;int i = 0;int num1 = 2; //生产线程数int num2 = 3; //消费线程数cout << "共有" << num1 << "个生产者线程, " << " 共生产出元素个数为:" << num1*test.num << endl;for (; i < num1; i++){Test t = test;t.id = i;_beginthreadex(NULL, 0, EnqueueThread, &t, 0, NULL);::WaitForSingleObject(hThreadEvent, INFINITE);::ResetEvent(hThreadEvent);}cout << "共有" << num2 << "个消费者线程" << endl;vector<Test*> vecTest;for (int j = i, k = 0; j < i + num2; j++, k++){Test *t = new Test;*t = test;t->id = j;vecTest.push_back(t);_beginthreadex(NULL, 0, DequeueThread, vecTest[k], 0, NULL);::WaitForSingleObject(hThreadEvent, INFINITE);::ResetEvent(hThreadEvent);}cout << "稍等..." << endl;Sleep(test.num / 5);int sum = 0;for (int j = 0; j < vecTest.size(); j++){sum += vecTest[j]->count;}cout << "所有消费者线程共消耗的元素为:" << sum << endl;cout << endl;cout << "请查看生成的元素是否刚好等于消耗的元素!!!如果相等调整null_num值再试试!" << endl;//释放资源for (auto it = vecTest.begin(); it != vecTest.end(); it ++) {if (NULL != *it) {delete *it; *it = NULL;}}vecTest.clear();UnInitQueue(queue);CloseHandle(hThreadEvent);}int main(){//当程序崩溃时,即是遇到了ABA问题,增大i的值,更有机会出现ABA//发生ABA可导致队列数据丢失等,此时程序不一定崩溃,但在本程序中//程序崩溃一定是遇到了ABAint num = 1000;for (int i = 0; i < num; i++){cout << "--------------共"<<num<<"次循环---------第【 " << i + 1 << " 】次循环.-------------------" << endl;cout << endl;test();cout << endl;}Sleep(30000);system("pause");return 0;}
ABA问题:
举个更生活化的例子:
土豪拿了一个装满钱的 Hermes 黑色钱包去酒吧喝酒,将钱包放到吧台上后,转头和旁边的朋友聊天,小偷趁土豪转头之际拿起钱包,将钱包里的钱取出来并放入餐巾纸保持钱包厚度,然后放回原处,小偷很有职业道德,只偷钱不偷身份证,土豪转过头后发现钱包还在,并且还是他熟悉的 Hermes 黑色钱包,厚度也没什么变化,所以土豪认为什么都没发生,继续和朋友聊天,直到结账时发现钱包中的钱已经被调包成餐巾纸。
所以,我觉得 ABA 问题还可以被俗称为 "调包问题"。那么怎么解决 "调包问题" 呢?土豪开始想办法了。
土豪想的第一个办法是,找根绳子将钱包绑在手臂上,要打开钱包就得先把绳子割断,割绳子就会被发现。这种做法实际上就是 Load-Link/Store-Conditional (LL/SC) 架构中所做的工作。
土豪想的另一个办法是,在钱包上安个显示屏,每次打开钱包显示屏上的数字都会 +1,这样当土豪在转头之前可以先记录下显示屏上的数字,在转过头后可以确认数字是否有变化,也就知道钱包是否被打开过。这种做法实际上就是 x86/64 架构中 Double-Word CAS Tagging 所做的工作。
土豪还担心小偷下次会不会买一个一模一样的钱包,直接调包整个钱包,这样连银行卡和身份证都丢了怎么办,土豪决定买一个宇宙独一无二的钱包,除非把它给烧了,否则就不会再有相同的钱包出现。这种做法实际上就是 Garbage Collection (GC) 所做的工作。
经过以上测试,大多数时候都正常,但偶尔会遇到ABA问题,这也是无锁队列的难点所在,因此以上代码时候问题的。
解决办法:
1、 初始化的时候分配若干内存,即内存池,入队时不需要new,而是从内存池中取出一块内存,由于出队时不会delete掉内存,所以不会出现ABA问题,但是内存池的大小应该多大?这也是需要考虑的问
题
2、使用Hazard指针,即自己实现一个垃圾回收的机制
3、
使用double-CAS(双保险的CAS),例如,在32位系统上,我们要检查64位的内容
1)一次用CAS检查双倍长度的值,前半部是指针,后半部分是一个计数器。
2)只有这两个都一样,才算通过检查,要吧赋新的值。并把计数器累加1。
这样一来,ABA发生时,虽然值一样,但是计数器就不一样(但是在32位的系统上,这个计数器会溢出回来又从1开始的,这还是会有ABA的问题)
当然,我们这个队列的问题就是不想让那个内存重用,这样明确的业务问题比较好解决,论文《Implementing Lock-Free Queues》给出一这么一个方法——使用结点内存引用计数refcnt!
SafeRead(q)loop:p q^:nextif p = NULL thenreturn pFetch&Add(p^:refct; 1)if p = q^:next thenreturn pelseRelease(p)goto loopend
其中的 Fetch&Add和Release分是是加引用计数和减引用计数,都是原子操作,这样就可以阻止内存被回收了。
更多知识可参考
http://www.cnblogs.com/catch/p/3176636.html
http://www.kuqin.com/algorithm/20120907/330193.html
http://www.lxway.com/4462682141.htm
http://blog.csdn.net/pongba/article/details/589864
http://blog.csdn.net/oygg2008/article/details/4524094
解决ABA问题,代码请见下一篇文章 点击打开链接
2、用数组实现无锁队列
本实现来自论文《Implementing Lock-Free Queues》
使用数组来实现队列是很常见的方法,因为没有内存的分部和释放,一切都会变得简单,实现的思路如下:
1)数组队列应该是一个ring buffer形式的数组(环形数组)
2)数组的元素应该有三个可能的值:HEAD,TAIL,EMPTY(当然,还有实际的数据)
3)数组一开始全部初始化成EMPTY,有两个相邻的元素要初始化成HEAD和TAIL,这代表空队列。
4)EnQueue操作。假设数据x要入队列,定位TAIL的位置,使用double-CAS方法把(TAIL, EMPTY) 更新成 (x, TAIL)。需要注意,如果找不到(TAIL, EMPTY),则说明队列满了。
5)DeQueue操作。定位HEAD的位置,把(HEAD, x)更新成(EMPTY, HEAD),并把x返回。同样需要注意,如果x是TAIL,则说明队列为空。
算法的一个关键是——如何定位HEAD或TAIL?
1)我们可以声明两个计数器,一个用来计数EnQueue的次数,一个用来计数DeQueue的次数。
2)这两个计算器使用使用Fetch&ADD来进行原子累加,在EnQueue或DeQueue完成的时候累加就好了。
3)累加后求个模什么的就可以知道TAIL和HEAD的位置了。
如下图所示:
- C++ 无锁队列 ABA <1>
- C++ 无锁队列 ABA <2>
- C++ 无锁队列 ABA <3>
- 无锁队列-使用hazard指针解决ABA问题
- 一个可无限伸缩且无ABA问题的无锁队列
- 用链表实现的无锁栈和无锁队列存在的ABA问题
- CAS操作实现并发的优势、以及实现一个无锁队列、怎样解决ABA 问题
- [c++]无锁队列
- [C]无锁循环队列
- 无锁编程(四) - CAS与ABA问题
- 无锁编程:lock-free原理;CAS;ABA问题
- [c/c++]使用宏函数实现的无锁队列
- aba
- 单生产者,单消费者无锁队列实现(c)
- [C++]只存储整型的无锁队列
- [C]只存储整形的无锁队列
- 无锁队列有关问题【1】
- 环形无锁队列-版本1
- C++<typeinfo>之typeid
- 我们应当怎样做需求分析
- Laravel框架
- Apache Spark探秘:多进程模型还是多线程模型?
- linux常用命令export
- C++ 无锁队列 ABA <1>
- dubbo zookeeper
- Why am I getting an AttributeError when trying to print out(转载)
- Spring Boot 配置日志输出等级
- c++ 内存分配过程(通过汇编,寄存器和Memory分析)
- 在RaspberryPi中用Pygame做信息显示屏
- 日志工具Log
- 当装了dreamweaver等某些IDE时,css失效了,其中1个原因
- dfafdsa