cas机制学习

1.介绍

CAS是英文单词Compare and Swap的缩写,翻译过来就是比较并替换。

CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B

更新时,比较之前读取到的A与现在的实际值V是否相等,若相等,则认为中间没有被更改过,将B赋值给B。

当V!=A时说明有其他线程抢先修改,会放弃当前操作,之后再重新查询V,重新尝试的过程被称为自旋(Spin)。

2.缺点

2.1 CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。(应该是自旋尝试的过程仍然占用cpu时间。)

2.2 不能保证代码块的原子性

https://segmentfault.com/a/1190000016611415

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。

2.3 ABA问题

第一个链接中提供了带图的例子,非常容易理解。

简单来说,就是有三个线程,编号为1、2、3,1和2同时查询获取当前值为A,都要修改为B,3查询值为B,要修改为A,执行顺序如下:

  • 1操作成功了,将A修改为B,但2阻塞住了;
  • 此时3查询值为B,成功将B修改为A;
  • 2能运行了,查询当前值为A符合条件,修改为B。

https://zhuanlan.zhihu.com/p/400817892

解决办法:

添加版本号。

真正要做到严谨的CAS机制,在compare阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。

那么当1和2在查询时,获取到A值和版本号1,当1修改完后,值为B,版本号为2,当3修改完后值为A,但版本号为3,在2开始执行时判断值虽然一致,但版本号不一致,所以更新失败。

3.例子

bool compare_and_swap (int *accum, int *dest, int newval)
{   
      if ( *accum == *dest ) 
      {       
           *dest = newval;       
           return true;   
      }   
      return false; 
} 

3.1 基于链表的非阻塞堆栈实现

// 栈数据结构
template  
class Stack { 
    typedef struct Node { 
                          T data;
                          Node* next;
                          Node(const T& d) : data(d), next(0) { } 
                        } Node;
    Node *top; // 一个节点,当前数据和指向下一个的指针
    public: 
       Stack( ) : top(0) { }
       void push(const T& data);
       T pop( ) throw (…); 
};

// 压入数据
void Stack::push(const T& data) 
{ 
    Node *n = new Node(data); 
    while (1) { 
        n->next = top;
        if (__sync_bool_compare_and_swap(&top, n->next, n)) { // CAS
            break;
        }
    }
}


// 弹出数据
T Stack::pop( ) 
{ 
    while (1) { 
        Node* result = top;
        if (result == NULL) 
           throw std::string(“Cannot pop from empty stack”);      
        if (top && __sync_bool_compare_and_swap(&top, result, result->next)) { // CAS
            return result->data;
        }
    }
}

4.CAS和自旋锁

自旋锁的实现依赖于 CAS 操作

  • 自旋锁是一种忙等待机制,线程在尝试获取锁时不会进入阻塞状态,而是不断地检查锁是否可用(通过不断“自旋”进行轮询)。
  • 在自旋锁中,线程会通过 CAS 操作不断尝试获取锁。通常的做法是用一个共享变量来表示锁的状态(如 0 表示未锁定,1 表示已锁定)。线程通过 CAS 检查这个变量是否为 0,如果是,则使用 CAS 操作将其置为 1,从而获取锁。如果 CAS 操作失败,说明其他线程已经获取了锁,当前线程则继续自旋直到获取锁。
std::atomic<int> lock_flag(0);  // 0 表示锁未被持有,1 表示锁已被持有

// 加锁
void acquire_lock() {
    while (!lock_flag.compare_exchange_weak(0, 1, std::memory_order_acquire)) {
        // 自旋,直到成功获取锁
    }
}

// 释放锁
void release_lock() {
    lock_flag.store(0, std::memory_order_release);  // 释放锁
}

自旋锁的优点与缺点

  • 优点
    • 短期锁定场景下,自旋锁效率很高,因为它避免了线程的上下文切换(上下文切换在阻塞型锁中发生,会有一定的性能开销)【线程切换时涉及到内核操作】。
    • 由于自旋锁不涉及内核操作,线程只在用户态进行自旋,减少了系统调用的开销。
  • 缺点
    • 忙等待:如果锁被一个线程长时间持有,其他线程会陷入无效的自旋消耗 CPU 资源。
    • 在高并发下,如果很多线程同时尝试获取锁,自旋锁的竞争会导致 CPU 负载增加。

自旋锁通常用于需要高性能和短期锁定的场景,特别是在实现一些无锁数据结构时,比如无锁队列、无锁栈等。利用 CAS 结合自旋锁,可以实现比传统互斥锁更高效的无锁同步机制。CAS 是一种乐观锁机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值