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的位置了。

如下图所示:


0 0
原创粉丝点击