Linux-读写锁原理-CAS无锁编程原理和缺陷

Posted 天津 唐秙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux-读写锁原理-CAS无锁编程原理和缺陷相关的知识,希望对你有一定的参考价值。

1. 读写锁

1.1 适用场景

  大量读,少量写

1.2 原理

  1. 多个程序可以并行的对临界资源进行读操作,程序不会产生二义性结果。
  2. 读写锁的内部有一个引用计数,来统计当前以读模式打开的读写锁的线程的数量。
  3. 通过引用计数来判断当前读写锁是否还有线程以读模式打开,判断什么时候读写锁是空闲的,没有被占用。

1.3 读写锁的三种模式

  1. 以读模式打开读写锁
  2. 以写模式打开读写锁
  3. 不加锁

1.4 面试题

问题: 线程A读模式,线程B读模式,并且已经获取读写锁,线程C想要以写模式打开,线程D想要以读模式打开,问线程D能不能直接获得读写锁?

不能,因为如果D能获取读写锁,那么我们假设后面全是想要获取读模式的线程,那么线程C将永远无法获取读写锁,因此,在将存在一个想要以写模式打开的线程的时候,后面想要获取读模式的线程将会进入等待队列,直到线程C解锁。

1.5 接口

1.初始化接口
  int pthread_rwlock_init
在这里插入图片描述
2.销毁接口
  int pthread_rwlock_destroy
在这里插入图片描述
3.读方式
  int pthread_rwlock_rdlock
  int pthread_rwlock_tryrdlock
在这里插入图片描述
4.写模式
  int pthread_rwlock_wrlock
  int pthread_rwlock_trywrlock
在这里插入图片描述
5.解锁接口
  int pthread_rwlock_unlock
在这里插入图片描述

2. CAS无锁编程

2.1 前言

  CAS(Compare And Swap,比较并交换),CAS根据其设计思想,可以划分为乐观锁。不同于synchronized关键字,synchronized实现的是悲观锁。乐观锁和悲观锁是基于线程并发竞争的角度来说的,悲观锁就是假设每次操作都是悲观的认为会发生线程竞争,不加锁就会导致程序结果错误,乐观锁就是假设每次操作都是乐观的认为不会发生线程竞争,所以不需要上锁,因此,CAS被称为无锁编程,实际上是一种乐观锁的体现。

2.2 代码

void* MyThreadStart(void* arg)
{
    RingQueue* rq = (RingQueue*)arg;
    for(int i = 0; i < 1000; i++)
    {
        rq->add();
        printf("%d\\n", rq->ret());
    }
    return NULL;
}

  两个线程累加同一个共享变量,线程不安全,输出的count小于等于2000,想要解决这个问题加锁就可以,但是如果要用无锁编程CAS来解决该怎么解决?

在这里插入图片描述
  现在无论运行多少遍,结果都是2000,也就是在没有加锁的情况下,写出了线程安全的count++操作,因此实现的关键就是AtomicInteger#getAndIncrement()方法。所以我们直接看下getAndIncrement()的源码。

/**
 * Atomically increments by one the current value.
 * 以原子方式将当前值增加一
 */
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

  可以该实现主要调用了this.compareAndSwapInt(…)方法,该方法就是便是CAS(Compare And Swap,比较并交换)。既然是比较和交换,那我们应该明确两点:比较什么、交换什么?
  CAS操作涉及到三个变量(V、E、N),V表示要更新的变量(工作内存中该变量的值)、E表示期望值(主内存中该变量的值)、N表示新值。
  首先判断变量当前值(V)是否等于期望值(E),不等于则说明在当前线程修改这个变量,同步回主内存之前,有别的线程已经修改过这个变量并且同步回了主内存。所以当前线程不能把值同步回主内存,而是重新从主内存中读取该值,重复这整个操作,直到当前值(V)等于期望值(E),才将新值同步回主内存。

2.3 过程描述

  1. 线程A从主内存中读入变量count作为值V;
  2. 线程A读取count的最新值,作为期望值E
  3. 线程A把值(V)和期望(E)比较是否相等,相等就把新值(N)写回主内存,不相等就回到操作1

  其中第三步是原子操作,比较V和E局势为了保证变量count没有被其他线程修改过。

2.4 CAS缺陷

  • 自旋的实现方式让所有线程都处于高频运行,争抢CPU的状态,如果操作长时间不成功,会带来很大的CPU消耗
  • 仅针对单个变量的操作,不能用于多个变量来实现原子操作
  • 会存在ABA问题

  ABA问题指主存当中的count变量从100变成101,再变成100的这种情况,这时候虽然还是100,但是这时候的100和原来100的含义不同,但是对于线程A是无法感知的,如果在实际开发当中不需要考虑ABA的影响,就可以使用CAS。

以上是关于Linux-读写锁原理-CAS无锁编程原理和缺陷的主要内容,如果未能解决你的问题,请参考以下文章

无锁编程:lock-free原理;CAS;ABA问题

并发01--并发存在的问题及底层实现原理

CAS无锁队列的实现

并发编程(学习笔记-共享模型之无锁)-part5

Linux(程序设计):24---无锁CAS(附无锁队列的实现)

并发编程之 CAS 的原理