CAS lockfree 循環隊列

在寫網絡爬蟲時涉及到多線程並行處理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 中, 再次重複以上過程.
轉載自:http://www.cnfn.org/cas-no-lock-queue.html
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章