使用CAS操作实现的无锁栈

CAS(compare and set),是原子数支持的一种操作,假如我们有一个原子数类,那么他其中的CAS函数大致可以理解为这样

bool compare_and_set(T&a,T&b)
{
    if(*this==a)
    {
        *this=b;
        return true;
    }
    else
    {
        a=*this;
        return false;
    }
}

并且可以保证上边这个函数是原子性的,我们可以通过这样一个函数在许多地方代替锁的使用,提高程序的速度,比如下面一个无锁栈。

template<typename T>
struct Node
{
	Tvalue;
	Node*next;
	Node(const T& data):value(data),next(NULL)
	{ }
};
template<typename T>
class free_lock_stack
{
private:
	atomic<Node<T>*>head;
public:
	free_lock_stack():head(NULL)
	{ }
	void push(const T& data)
	{
		Node* new_node=new Node(data);
		new_node->next=head.load();
		while(!head.compare_exchange_weak(new_node->next,new_node))
			;
	}
	T pop()
	{
		Node*result=head.load();
		while(!result&&!head.compare_exchange_weak(result,result->next))
			;
		assert(result);
		T temp=result->value;
		free(result);
		return temp;
	}
};

C++11提供的原子数支持以下五种操作

  • store
  • load
  • swap
  • compare_exchange_weak
  • compare_exchange_strong

分别是原子写,原子读,原子交换和两种CAS操作,这两种CAS操作本质上并没有什么区别,只是weak版本的在实例函数return true的时候也可能return false,在循环里使用并不影响程序的正确性,但是其效率比strong,也就是不会出错的版本高。

可是原子操作实现的无锁变成也有自己的缺点,他虽然可以保证单步是原子的,但还是无法保证整个函数的实现是原子的,这将导致下面两种情况下的错误执行。

  • 比如目前栈内情况是这样的 top->A->B->NULL,线程1要执行pop操作,当执行完load后切换到线程2,线程2把A和B都pop走,并且push了一个A,这个新的A可能由于glibc层内存分配策略的原因,和原先的A有相同的地址(ptmalloc就是这样),这就导致线程1错误的认为这个A和之前的A是一样的,因为毕竟是用指针来检查的,拥有相同的地址便很自然的认为是同样的,于是线程1决定popA,并且设置B为栈顶,但是并不知道B已经被free了。
  • 再或者当pop时,获得了头指针之后,切换到别的线程,把这个节点删除,这样几乎一定会导致错误。

虽然以上的问题也有解决的方案,不过若想避免互斥锁带来的系统调用负担,使用原子数封装的自旋锁+yield的效率和无锁编程比效率也差不多,而且不会有上边的问题出现。

猜你喜欢

转载自blog.csdn.net/qq_33113661/article/details/89072355
今日推荐