CAS lockfree 循环队列

来源:互联网 发布:400电话网站模版源码 编辑:程序博客网 时间:2024/06/05 17:37

在写网络爬虫时涉及到多线程并行处理URL的问题, 开始打算给相关数据加锁来解决该问题, 之后考虑到锁是会影响性能的, 虽然处理URL的那部分不是这种小型爬虫的瓶颈所在(网速才 是最大的瓶颈啊), 但能更快一点岂不更好? 所以就想使用无锁技术.

通过查阅资料, 参考陈皓老师的无锁队列的实现 和淘宝搜索技术博客的一种高效无锁内存队列的实现, 使用CAS(compare and swap, 比较交换)技术和数组模拟实现无锁队列.

CAS 操作是在一个原子操作内完成的.

使用 CAS 需要提供三个参数, 分别为:

要修改的变量var;存储在执行 CAS 操作之前变量 var 的原值 old_var;变量 var 的目的值 new_var.

CAS 操作处理过程:

  • 在执行 CAS 操作前, 需要先获取变量 var 的值并存储到 old_var 中.
  • 之后执行 CAS 操作: 检查 var 是否与 old_var 相等. 如果 var == old_var, 则说明没有其它线程修改 var, 此时可将 new_var 赋值给 var. 如果 var != old_var, 则说明在当前线程保存 var 的值后有其它线程修改了变量 var, 此时应重新获取变量 var 的值并保存至 old_var 中, 再次重复以上过程.

CAS的ABA问题主要出现在动态分配内存的情形, 使用数组模拟实现无锁队列时, 每一个存储空间都是固定不变的, 所以就不需要考虑这个问题了.

使用CAS技术和数组模拟实现无锁队列时需要注意的问题:

* 利用数组模拟循环队列时, 数组大小是固定的, 进队和出队操作要齐头并进, 不能存在较大时间差. 如果有一段时间只入队而不出队, 则在队列满之后, 入队操作将会被阻塞, 类似于死锁. 所以当出入队时间存在较大时间差时可考虑动态分配存储空间. * 利用数组模拟循环队列时, 通过取余操作定位下标的效率较低. 可将数组大小设置为2的指数倍, 之后通过位操作确定下标, 如 index & (size - 1) 的形式定位数组下标.

以下代码是我用C++实现的无锁队列模板, 提供如下接口:

Enqueue: 入队Dequeue: 出队set_enqueue_done: 设置入队完毕标识get_is_enqueue_done: 检查是否入队完毕get_is_denqueue_done: 检查是否出队完毕get_enqueue_num: 获得最新的入队编号get_dequeue_num: 获取最新的出队编号

代码:

// 采用 CAS 技术, 用数组实现的无锁队列,// 可多线程入队出队.#ifndef _QUEUE_H_#define _QUEUE_H_#include <atomic>#include <boost/thread.hpp>using namespace std;using namespace boost;template<typename T>class Queue{public:Queue(const int &size = 16384);~Queue() { delete [] m_data; }long Enqueue(const T &value);long Dequeue(T &value);void set_is_enqueue_done(const bool &is_enqueue_done){ m_is_enqueue_done = is_enqueue_done; }bool get_is_enqueue_done() const { return m_is_enqueue_done; }bool get_is_dequeue_done() const { return m_is_dequeue_done; }long get_dequeue_num() const { return m_dequeue_num; }long get_enqueue_num() const { return m_enqueue_num; }void Clear();private:int m_size;bool m_is_enqueue_done;bool m_is_dequeue_done;volatile atomic<long> m_enqueue_num;volatile atomic<long> m_dequeue_num;T *m_data;void set_size(const int &size);};template<typename T>Queue<T>::Queue(const int &size /* = 16384 */){set_size(size);m_data = new T[m_size + 1];m_is_enqueue_done = m_is_dequeue_done = false;m_enqueue_num = m_dequeue_num = 0;}template<typename T>void Queue<T>::set_size(const int &size){    if (size <= 16384){m_size = 16384;return;}m_size  = 16384;while (m_size < size){m_size <<= 1;}    m_size >>= 1;}template<typename T>long Queue<T>::Enqueue(const T &value){while (m_enqueue_num - m_dequeue_num >= m_size)this_thread::sleep(posix_time::seconds(1)); // this_thread::yield();    long old_num;do{enqueue_loop:old_num = m_enqueue_num;if (-1 == m_enqueue_num){this_thread::sleep(posix_time::seconds(1)); // this_thread::yield();goto enqueue_loop;}} while (!atomic_compare_exchange_weak(&m_enqueue_num, &old_num, (long)-1));m_data[old_num & (m_size - 1)] = value;    m_enqueue_num = old_num + 1;return m_enqueue_num;}template<typename T>long Queue<T>::Dequeue(T &value){long old_num_, new_num;do{dequeue_loop:old_num_ = m_dequeue_num;new_num= old_num_ + 1;if (m_dequeue_num >= m_enqueue_num){if (m_is_enqueue_done){m_is_dequeue_done = true;return 0;}elsethis_thread::sleep(posix_time::seconds(1)); // this_thread::yield();goto dequeue_loop;}value = m_data[m_dequeue_num & (m_size - 1)];} while (!atomic_compare_exchange_weak(&m_dequeue_num, &old_num_, new_num));return m_dequeue_num;}template<typename T>void Queue<T>::Clear(){m_enqueue_num = m_dequeue_num = 0;m_is_enqueue_done = m_is_dequeue_done = false;}#endif/* _QUEUE_H_ */
总结:1.循环队列使用线性表实现,少用一个节点区分空和满的情况,队头和队尾等为空,(队尾+1)%size==队头为满。
取余操作效率较低,可将数组大小设置为2的指数倍,可通过index&(size-1)形式定位数组下标。
2.lockfree关键是保障队头队尾指针atomic

使用 CAS 需要提供三个参数, 分别为:

要修改的变量var;存储在执行 CAS 操作之前变量 var 的原值 old_var;变量 var 的目的值 new_var.

CAS 操作处理过程:

  • 在执行 CAS 操作前, 需要先获取变量 var 的值并存储到 old_var 中.
  • 之后执行 CAS 操作: 检查 var 是否与 old_var 相等. 如果 var == old_var, 则说明没有其它线程修改 var, 此时可将 new_var 赋值给 var. 如果 var != old_var, 则说明在当前线程保存 var 的值后有其它线程修改了变量 var, 此时应重新获取变量 var 的值并保存至 old_var 中, 再次重复以上过程.
转载自:http://www.cnfn.org/cas-no-lock-queue.html