自旋锁、互斥锁、读写锁、悲观锁、乐观锁

前言

本文属于搬运: https://blog.csdn.net/u014630623/article/details/106450230

​ 在开发的过程中,很常见的场景就是在多进程或者多线程中访问同一份资源,而如果直接不加限制的对这段资源进行写操作的话,很可能会将这段共享资源写乱而导致不可预期的后果。在Linux中为了解决这个问题,一个常用的方法就是对操作这段共享资源的区域进行加锁避免上述问题。在Linux中将锁在不同的角度进行了一些分类,这里记录一下Linux中提到的一些锁的概念以及其特点。

本文只对部分类型的锁的概念、特点进行记录,而不深究其实现。如果有什么理解有误的地方欢迎指正。

 

自旋锁(spinlock)与互斥锁

概念
​ 自旋锁设计的初衷是在短期内进行轻量级的锁定(后面介绍自旋锁的特点就能清楚的明白为什么是在短期内了),它将一段临界区代码进行锁定,保证当前线程/进程执行这段临界区代码的时候不会被其他线程/进程中断,避免因为竞争导致共享资源被破坏。自旋锁被广泛地运用在Linux底层的同步机制,你可以看到许多内核的数据结构中都嵌有spinlock,这些大部分是用于保证它自身被操作的原子性,在操作这些结构的时候通常都会经过这几个步骤:加锁->操作->解锁。

​互斥锁一般通过互斥量mutex来实现,互斥锁的实现也是为了避免多进程/多线程访问共享资源的时候,由于数据竞争而导致的数据被破坏而设计的。通常程序中我们通过互斥量对临界区进行加锁,临界区代码执行完成后,对其进行一个解锁操作。

​ 从上面看自旋锁和互斥锁的功能基本上类似,但是它们之间的区别是什么呢?首先我们描述一下锁的一个工作过程:

1、初始状态下,锁的状态为未被占有
2、A进程需要访问临界区代码,尝试获取锁。此时发现锁可用,则将锁lock,进入临界区执行临界区代码
3、B进程需要访问临界区代码,尝试获取锁。此时发现锁呢已经被A占有,则等待A释放锁
4、A进程退出临界区,释放锁.unlock
5、B进程获得锁进入执行临界区代码,并lock
6、B进程执行晚临界区代码,释放锁unlock
对于自旋锁和互斥锁,主要的区别体现在第3步和第5步。

第3步,当进程B发现锁已经被进程A占有:对于自旋锁来说,B进程会"原地旋转",即执行循环,去检测锁是否已经被释放;对于互斥锁来说,B进程直接进入sleep休眠状态,将CPU的使用权交由其他进程处理,等待锁被释放是被唤醒。

第5步,B进程被触发获得锁的方式:对于自旋锁来说,B进程自己检测待锁已经处于无人占有的状态,敏感度较高;而对于互斥锁来说,B进程是被系统重新唤醒,敏感度较差。

特点
1、自旋锁等待过程中耗费CPU,而互斥锁不会(原因:自旋锁等待锁的过程中循环检测锁的状态,)
2、自旋锁常用在临界区较为短小的场景下 (原因:等待自旋锁的时间过长,会浪费过多CPU)
3、自旋锁中等待的进程/线程获取锁的灵敏度较高(原因:自身循环检测)
4、自旋锁在非抢占式的单核处理器中不起作用。(原因:等待线程在循环等待,但是又不允许抢占,会导致CPU卡主,所以这种架构的处理器中自旋锁被实现为空)
5、自旋锁和互斥锁都只允许一次只有一个进程/线程进入临界区。
6、自旋锁在"唤醒"时不需要进行上下文切换,而互斥锁需要进行上下文切换,切换成本较高。

乐观锁与悲观锁

概念
​ 与自旋锁&互斥锁不同的是,乐观锁和悲观锁不是哪种类型的锁的实现,而是操作共享数据的一种思想。

​ 悲观锁以一种悲观的思想,认为对数据进行操作(读取/更新)的时候,很大可能会有其他进程/线程会对该数据进行修改。所以在悲观锁思想中,对数据进行操作之前先对数据进行加锁。只有获取锁成功,才允许对该数据进行操作;若获取锁失败则被阻塞或者报错。

​ 乐观锁则以一种乐观的思想,认为对数据进行操作的时候,数据不会被其他进程或者线程修改。乐观锁对于读取数据的场景,读取前不需要加锁,直接读取数据;对于更新数据的场景,也不进行加锁,而是通过版本号控制或者CAS的方式对数据进行更新。

备注:cas(compare and set)机制更新数据的机制的过程简单描述如下:首先客户端读取出当前数据的当前版本,然后本地对数据进行修改,修改完毕后,将数据和版本号同时发送到服务端进行更新,服务端发现版本和当前server端数据版本不一致,则说明server端数据被修改过,则直接报错不允许更新;若版本一致则更新成功。

特点
1、读多写少的场景下,建议使用乐观锁,此时乐观锁效率要高于悲观锁,可以提高系统的吞吐量
2、写多读少(写数据冲突较高)的场景下,建议使用悲观锁,因为写冲突高会导致cas产生大量碰撞从而导致大量失败重试
3、乐观锁更新数据存在ABA的问题:客户端取到数据为A,在更新的时候检查数据内容也是A,在客户端角度数据没有发生变化,但是有可能数据中间被修改成了B然后又变成了A。
4、乐观锁中只能保证一个共享变量的原子操作 CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。
5、乐观锁如果使用自旋cas(概念与上一节类似),如果更新长时间不成功,CPU资源会浪费比较大。

读写锁

概念

​ 在很多场景中,对共享数据的操作进行读操作较多,写操作很少;由于在读数据的时候不会产生脏数据,所以多线程同时对数据进行读操作是没有问题的。所以在这种场景下如果一味的使用互斥锁,会导致对数据的操作效率很低。因此这种情况下产生了读写锁,读写锁与互斥锁类似,也是对进程/线程进入一段临界区的访问控制。

特点
读写锁有三种状态:添加读锁,添加写锁,未加锁

1、只要没有添加写锁,所有读线程都允许进入临界区进行操作。(允许多读,写的时候不允许读)
2、只有在未加锁状态下,才允许写线程进入临界区。(只允许一个进程写,写的过程中不允许有其他任何操作)
3、Linux中pthread_rwlock_t读写锁本质上是一个自旋锁。
4、尽量在读多写少的场景下使用读写锁
5、在读和写操作进行竞争锁的时候,写操作优先获得锁。

其他博客链接: https://cloud.tencent.com/developer/article/1886477

 

posted @ 2022-06-29 11:47  萤huo虫  阅读(300)  评论(0编辑  收藏  举报