在寫網絡爬蟲時涉及到多線程並行處理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;
}
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 操作處理過程:
- 在執行 CAS 操作前, 需要先獲取變量 var 的值並存儲到 old_var 中.
- 之後執行 CAS 操作: 檢查 var 是否與 old_var 相等. 如果 var == old_var, 則說明沒有其它線程修改 var, 此時可將 new_var 賦值給 var. 如果 var != old_var, 則說明在當前線程保存 var 的值後有其它線程修改了變量 var, 此時應重新獲取變量 var 的值並保存至 old_var 中, 再次重複以上過程.