两种加锁思想(悲观锁,乐观锁),自旋锁(理解+接口),读写锁(理解,接口+伪代码,读者/写者优先的理解+设置)

本文探讨了并发编程中的悲观锁、乐观锁和自旋锁原理,包括它们的思想、实现方式和适用场景。特别关注了自旋锁的特性,以及如何通过pthread_mutex和read-write锁接口进行操作。
摘要由CSDN通过智能技术生成

目录

引入

两种加锁思想

悲观锁

思想

实现方式

使用场景

乐观锁

思想

实现方式

使用场景

自旋锁

自旋

引入

使用场景

接口

pthread_mutex_trylock()

pthread_spin_*

创建与销毁

​编辑

加锁和解锁

读写锁

引入

概念

理解

321原则

接口

创建和销毁

加锁和解锁

伪代码理解

读者优先和写者优先

pthread_rwlockattr_setkind_np 

attr

pref


引入

  • 在计算机科学中,锁是一种同步机制,用于控制对共享资源的访问
  • 锁可以防止多个线程或进程同时访问临界区,从而防止数据竞争和保护共享资源的一致性
  • 前面我们已经学习了互斥锁,但其实还有很多锁类型

两种加锁思想

悲观锁和乐观锁是并发控制中两种不同的策略,用于处理多个线程或进程同时访问共享资源的情况

悲观锁

思想

  • 在整个数据处理过程中,会悲观地担心数据会被其他线程修改
  • 因此在访问共享资源之前,先进行加锁,确保在同一时刻只有一个线程或进程能够访问该资源
  • 而同时想要进入的其他线程或进程,需要等待锁的释放

实现方式

比如互斥锁,数据库中的锁机制(行级锁、表级锁等)

使用场景

在并发度较低、冲突概率较高的情况下,悲观锁可能更为合适

乐观锁

思想

  • 乐观的认为在整个数据处理过程中,数据不会被其他线程修改
  • 因此,不在访问共享资源之前加锁,而是在实际更新数据时检查是否发生了冲突
  • 如果没有冲突,则更新成功;如果发生了冲突,采取相应的处理策略(例如重试或放弃更新)

实现方式

  • 通常使用版本号(Version Number)或时间戳等机制
  • 每次更新时都会比对当前版本号([当前数据的版本号]是否与线程[开始时读取的版本号]一致)
  • 只有在版本号匹配的情况下(说明没有其他线程修改过),才能成功更新

使用场景

在并发度较高、冲突概率较低、以及希望减少加锁对系统性能影响的情况下,乐观锁可能更为合适

自旋锁

自旋

  • 自旋是一种并发编程中的等待机制
  • 其基本思想是 -- 在等待共享资源的线程中,通过循环检查资源是否可用,而不是立即进入阻塞状态
  • 这个循环过程被称为自旋

引入

  • 从前我们的加锁策略都是 -- 如果申请锁失败,就阻塞当前线程,直到对应锁释放
  • 这种锁被称为挂起等待锁

  • 但是,阻塞和唤醒线程都是要花费时间的
  • 如果出现这种情况 -- 需要访问的临界区很小,其他线程很快就释放了锁
  • 这时候再采取上面的措施,就有点浪费时间(可以但没必要)
  • 就像你询问家人什么时候可以出门,对方说我马上就出门,而你此时选择开把游戏
  • 很不合理对吧,明明对方很快就会出去
  • 所以就出现了自旋锁 -- 不断询问对方是否完成,让自身处于随时就绪的状态,就可以减少中间的成本浪费

使用场景

取决于其他线程在临界区中运行的时间长短

  • 如果长 -- 就使用挂起等待锁
  • 如果短 -- 就使用自旋锁

接口

pthread_mutex_trylock()

如果将这个函数外面套一层while循环,就可以实现自旋

 

当然,linux也提供了直接的自旋锁接口(spin系列函数)

pthread_spin_*

和pthread_mutex_t类型一样,标识自旋锁的类型为pthread_spinlock_t

创建与销毁
  • pshared用于指定自旋锁的共享性质,通常设置为0

当然,也可以静态获取自旋锁: PTHREAD_SPINLOCK_INITIALIZER

加锁和解锁

其中:

  • lock不断自旋,直到自旋锁被释放
  • trylock在自旋锁被其他线程持有时,会立即返回

 

读写锁

引入

  • 在编写多线程的时候,有一种情况是十分常见的 -- 有些公共数据修改的机会比较少,读的机会反而高的多 ,也就是读者写者问题
  • 通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长,如果给这种代码段加锁,会降低我们程序的效率
  • 为了解决这种多读少写的情况,读写锁的概念就被提出了

概念

  • 是一种多线程同步机制,允许多个线程同时对共享资源进行读操作,但只允许一个线程进行写操作
  • 这样可以提高并发性,因为多个线程可以同时读取数据而无需相互等待
理解

就比如,班级上需要办黑板报(但这里是小的那种,只能容纳一个人的空间去修改)

  • 那么,每次只能有一个人去画
  • 而班级是有空间的,可以允许多名同学观看黑板报

但是,如果同学在画的时候,有其他同学在看

  • 看的同学只能得到部分的信息,这对双方都不公平(读者可能会因为部分的信息而误解写者的意图)
  • 所以,写者在写的时候,不允许读者观看

同理,有其他同学在看时,有同学直接把它擦掉了

  • 这样对看的同学不公平
  • 所以,读者在读的时候,不允许写者修改

321原则

和之前学习生产消费者模型一样:生产消费者模型(引入--超市),321原则,阻塞队列实现+优点(代码,伪唤醒问题,条件变量接口wait中锁的作用),进阶版实现(生产任务,RAII风格),多生产多消费实现+优点-CSDN博客读者写者问题也可以总结为321原则

  • 1个场所 -- 公共资源
  • 2个角色 -- 读者,写者
  • 3种关系 -- 读者与读者(共享),读者与写者(互斥),写者与读者(互斥)

接口

读写锁的数据类型是pthread_rmlock_t

创建和销毁

attr为读写锁的属性,通常设置nullpt,表示使用默认属性

加锁和解锁

加锁分为读者加锁,写者加锁

不同的对象使用不同的加锁方式:

不同的加锁方式,遇到不同的锁状态,其行为也不同:

而解锁是通用的:

伪代码理解

我们使用伪代码来模拟一下加读者锁和写者锁:

  • 因为读锁允许多个线程同时进入临界区,所以读取过程不进行加锁(也就是读锁申请时,不阻塞其他读锁请求)
  • 但是,为了保证读取期间没有写者打扰,需要在第一个读者进来时,申请写者锁(也就是阻塞写锁请求,保证了读写之间的互斥)
  • 注意,访问记录读者数量这个临界资源时,需要加锁保护
  • 当读取过程中的最后一个读者离开时,需要释放写锁

  • 而写者加锁很简单,就是在加锁区域内进行写入即可(这样就保证了写者之间的互斥)
  • 因为读者访问期间,写者是阻塞状态,所以不需要额外操作就可以保证与读者互斥
  • 而写者访问过程中,会让读者卡在申请写锁那里(也就保证了写读互斥)

读者优先和写者优先

由于读者比写者多的原因,读者竞争锁的概率会更高,自然会形成读者优先的局面

  • 这是本身就存在的饥饿问题,是正常的
  • 这里的饥饿问题是一个中性的概念,并不是平时说的那种

如果想要设置成写者优先(这样更公平一些),可以使用pthread_rwlockattr_setkind_np函数

pthread_rwlockattr_setkind_np 

attr
  • 读写锁属性对象的指针
  • 通过pthread_rwlockattr_init来初始化:
pref
  • 用于设置读写锁的类型
  • (似乎第二个有bug,和读者优先有一样的表现)
  • 默认类型取决于特定系统和库的实现

 

  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值