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的效率和无锁编程比效率也差不多,而且不会有上边的问题出现。