在写网络爬虫时涉及到多线程并行处理URL的问题, 开始打算给相关数据加锁来解决该问题, 之后考虑到锁是会影响性能的, 虽然处理URL的那部分不是这种小型爬虫的瓶颈所在(网速才 是最大的瓶颈啊), 但能更快一点岂不更好? 所以就想使用无锁技术.
通过查阅资料, 参考陈皓老师的无锁队列的实现 和淘宝搜索技术博客的一种高效无锁内存队列的实现, 使用CAS(compare and swap, 比较交换)技术和数组模拟实现无锁队列.
CAS 操作是在一个原子操作内完成的.
使用 CAS 需要提供三个参数, 分别为:
要修改的变量var;
存储在执行 CAS 操作之前变量 var 的原值 old_var;
变量 var 的目的值 new_var.
CAS 操作处理过程:
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;
}
else
this_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 操作处理过程: