自旋锁和读写锁

news2024/11/27 8:40:50

目录

一、自旋锁

1.自旋锁和挂起等待锁

2.自旋锁的接口

二、读写锁

1.读者写者模型与读写锁

2.读写锁接口

3.加锁的原理

4.读写优先级


一、自旋锁

1.自旋锁和挂起等待锁

互斥锁的类型有很多,我们之前使用的锁实际上是互斥锁中的挂起等待锁。互斥锁比较有代表性的锁类型是挂起等待锁和自旋锁,在这里我们讲解他们的不同。

  • 多线程在竞争一个挂起等待锁时,申请到锁的线程会进入临界区,而没有申请到锁的线程会被放入操作系统维护的等待队列中,也就是阻塞等待。在合适的时候,操作系统会将放到运行队列中继续申请锁。
  • 互斥锁也能保护共享资源的安全。多线程以同样的方式竞争自旋锁,申请到锁的线程会进入临界区,而没有申请到锁的线程会继续不停申请锁,直到申请锁成功进入临界区,与进程的轮询等待非常的相似。
  • 挂起等待锁会将未申请到锁的线程挂起,CPU暂时就不需要调度它,可以去执行其他任务。而未申请到自旋锁的线程会不断申请,始终占用CPU资源。

那为什么会出现自旋锁呢?

因为线程等待的时长决定了线程等待的方式。

当线程访问临界资源的时间较短的时候,就可以使用自旋锁。挂起线程再唤醒线程的过程也是有不小的开销的,所以执行时间短的线程如果保持自旋状态,线程免去了挂起的过程,可以很快进入临界区,一定程度上提高了效率。

当线程访问临界资源的时间较长的时候,就要使用挂起等待锁。对运行时间长的线程,将申请锁失败的线程挂起,CPU资源就空闲了出来。此时如果使用自旋锁,CPU一直被占用,效率就会下降。

那等待的时间长短怎么衡量呢?

  • 对于处理简单的线程,比如前面的多线程抢票代码,对tickets的访问就可以使用自旋锁。
  • 对于需要进行复杂运算,高IO以及等待某些软件标志就位的情况,就必须使用挂起等待锁。

上面的描述只是一种经验,等待时间的长短并没有明确的定义。自旋锁和挂起等待锁的取舍需要根据具体情况选择。最简单有效的方式就是分别测试两种锁,哪种效率高就用哪种。

2.自旋锁的接口

以下是自旋锁(pthread_spinlock_t)的一些成员函数,与挂起等待锁基本一致:

int pthread_spin_init(pthread_spinlock_t* lock, int shared);

头文件:pthread.h

功能:初始化互斥锁。

参数:pthread_spinlock_t* lock表示需要被初始化的自旋锁的地址。int shared表示锁的是否进程间共享,0表示共享,非0表示不共享,一般都设为0。

返回值:取消成功返回0,取消失败返回错误码。

int pthread_spin_destroy(pthread_spinlock_t* lock);

头文件:pthread.h

功能:销毁互斥锁。

参数:pthread_spinlock_t* lock表示需要被销毁的自旋锁的地址。

返回值:销毁成功返回0,失败返回-1。

int pthread_spin_lock(pthread_spinlock_t* lock);

头文件:pthread.h

功能:对lock到unlock的部分代码加锁(仅允许线程串行)。

参数:pthread_spinlock_t* lock表示需要加锁的锁指针。

返回值:加锁成功返回0,失败返回-1。

int pthread_spin_unlock(pthread_spinlock_t* lock);

头文件:pthread.h

功能:标识走出lock到unlock的部分代码解锁(恢复并发)。

参数:pthread_spinlock_t* lock表示需要解锁的锁指针。

返回值:解锁成功返回0,失败返回-1。

我们下去可以把之前写的抢票代码改出一个自旋锁版本,使用上就不强调了。

二、读写锁

1.读者写者模型与读写锁

读写锁主要应用于读者写者模型,读者写者模型和生产者消费者模型很相似,也遵循321原则。

  • 三种关系:写者和写者(互斥),读者和写者(同步和互斥),读者和读者(没关系)。
  • 两种角色:读者和写者。
  • 一个交易场所:任意类型的数据结构。

读者线程和写者线程并发访问一块临界资源:

  • 写者向临界资源中写数据。
  • 读者从临界资源中读数据。
  • 读者和写者之间是互斥关系

这里只有三种关系不太好理解:

(1)写者和写者直接是互斥关系。

如果一个写者正在写数据,另一个写者也来写。他们如果写的是同一块资源,就有可能发生覆盖,数据就会出错。

(2)读者和读者之间没有关系。

读者只从临界区中读取数据,并不改变临界资源,所以读者之间不会相互影响。

(3)写者和读者是互斥关系,也是同步关系。

写者在写数据时,如果允许读者读取,读者读到的数据就会不全。所以写者写数据时,读者不能读数据,它们是互斥关系。

写者向数据结构中写数据,读者从中读数据,二者动态平衡才能维持程序运行。所以它们也是同步关系。

读者写者模型可以在哪些场景下使用?

这种模型适用于:一次发布且很长时间不修改,大部分时间都是在被读取,比如你现在看的这篇博客。

所以,读者写者模型与生产者消费者模型最大区别就是消费者会拿走临界资源中的数据,而读者不会。

那这跟读写锁又有什么关系呢?

有些共享资源的数据和我们的博客一样,很少修改,反而需要频繁读取,它们被读取的机会比被修改高得多。

在读取这种数据的时候,往往需要大量的查找时间,如果我们再给这样的代码加锁,那么程序的效率将会严重下降。

读写锁就是专门用于读者写者模型中的一种锁,可以维护读者写者的321原则。(这里不太理解的话可以看后面的伪代码)

临界区的状态

读者申请

写者申请

不加锁

可以

可以

读锁

可以

阻塞

写锁

阻塞

阻塞

持有写锁的线程独占临界资源,持有读锁的线程,读者之间共享临界资源。

2.读写锁接口

以下是自旋锁(pthread_rwlock_t)的一些成员函数:

int pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattrt_t* attr);

头文件:pthread.h

功能:初始化互斥锁。

参数:pthread_rwlock_t* rwlock表示需要被初始化的读写锁的地址。const pthread_rwlockattrt_t* attr表示读写锁属性结构体指针,一般设置成nullptr即可。

返回值:取消成功返回0,取消失败返回错误码。

int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);

头文件:pthread.h

功能:销毁互斥锁。

参数:pthread_rwlock_t* rwlock表示需要被销毁的读写锁的地址。

返回值:销毁成功返回0,失败返回-1。

int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);

头文件:pthread.h

功能:对lock到unlock的部分代码加读锁(仅允许线程串行)。

参数:pthread_spinlock_t* lock表示需要加读锁的锁指针。

返回值:加读锁成功返回0,失败返回-1。

int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);

头文件:pthread.h

功能:对lock到unlock的部分代码加写锁(仅允许线程串行)。

参数:pthread_spinlock_t* lock表示需要加写锁的锁指针。

返回值:加写锁成功返回0,失败返回-1。

int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);

头文件:pthread.h

功能:标识走出lock到unlock的部分代码解锁(恢复并发)。

参数:pthread_spinlock_t* lock表示需要解锁的锁指针。

返回值:解锁成功返回0,失败返回-1。

3.加锁的原理

在任何时刻,读写锁只允许一个写者写入,但允许多个读者并发读取(读者读时写者阻塞)。

这是不是就很奇怪,明明锁应该只有一把,怎么还出了读锁和写锁,读锁还能同时给多个线程。

读写锁(pthread_rwlock_t)是一个结构体,它封装的也是互斥锁。只是针对读者有一把,针对写者有一把,二者申请的思路不同罢了。

加读锁伪代码: pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);

pthread_mutex_t rdlock;//创建读锁
int reader_count = 0;//读者计数
------------------------------------------------------------
lock(&rdlock);//读加锁
reader_count++;//读者数量加一
if(reader_count == 1)
{
    //只要有读者在访问临界资源,就将写锁也申请走
    lock(&rwlock);//写加锁
}
unlock(&rdlock);//解读锁
------------------------------------------------------------
//读取数据....
------------------------------------------------------------
lock(&rdlock);//再次加读锁
read_count--;//读者数量减一
if(reader_count == 0)
{
    //读者全部读完以后,释放写锁
    unlock(&rwlock);//写解锁
}
unlock(&rdlock);//读解锁

伪代码的含义:

加读锁时,有一个所有读者线程共享的计数器,用来统计访问公共资源的读者数量。

每个读者访问公共资源时,都需要将计数值加1,考虑到线程安全,所以计数值要加锁。

当第一个读者到来后,它会先申请读锁,然后申请写锁。如果第二个读者线程也要申请读锁,只需要将计数器加一即可。此时,由于写锁在读者手里,写者线程申请不到写锁,也就无法访问临界资源了。

一个读者读完数据后,计数器将通过原子性的操作将值将减一。当值被减到0时,说明没有读者再来读数据了。此时写锁会被解锁,写者线程就可以申请写锁并输入数据。

通过这样的方式就实现了读者和写者间的互斥,也使得读者线程可以并发执行。

加写锁伪代码: pthread_rwlock_wrlock(pthread_rwlock_t* lock);

pthread_mutex_t wrlock;//创建写锁
------------------------------------------------------------
lock(&wrlock);//写加锁
//向临界资源中写入数据
unlock(&wrlock);//写解锁

写者间直接使用挂起等待锁,实现了写者间的互斥关系。

4.读写优先级

上面的模型中,读线程一旦申请到锁,那么写锁也被同时被申请走了。除了所有读线程都未在读取数据时,读线程和写线程可以根据自己的竞争能力申请锁。如果有读者在访问共享资源,此时读写锁就是读者优先的。

如果读者非常多,那读者申请到锁的几率会非常大,而且读者还会抱着写锁不放。写者申请不到锁,始终无法进入临界区访问临界资源,就会导致写者饥饿问题。所以说,读写锁很适合管理读取次数多,修改次数极少的数据。

读写锁虽然设计上更偏向于读者,但也是可以设置成写者优先的。

大体逻辑是这样的:读者申请到读锁和写锁并访问公共资源,即使写锁已经被申请,写线程也会被调度。

根据线程的运行顺序,系统将会把写线程后的所有读线程阻塞,不允许它们访问公共资源。

当前面的所有读线程都读完数据后,由于挂起的读线程不会加到计数器上,所以此时计数器数字为0。读线程归还写锁,写线程申请到读锁后开始写入数据。

pthread库就已经提供了设置读写锁的读写优先级的接口:

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr, int pref);

头文件:pthread.h

功能:设置读写优先级

参数:pthread_rwlockattr_t* attr是一个读写锁设置锁属性的结构体指针,pref是读者写者优先选项,有三种选项。

PTHREAD_RWLOCK_PREFER_READER_NP:(默认设置)读者优先,可能会导致写者饥饿情况。

PTHREAD_RWLOCK_PREFER_WRITER_NP:写者优先

PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:写者优先,但写者不能递归加锁。

返回值:加读锁成功返回0,失败返回-1。

此时我们关于自旋锁和读写锁的问题就都解决了,这一章一方面操作于原来使用的挂起等待锁基本一致,另一方面,我们能使用的地方不多。所以希望大家能尝试一些代码练习以巩固知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/978239.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JMeter(三十九):selenium怪异的UI自动化测试组合

文章目录 一、背景二、JMeter+selenium使用过程三、总结一、背景 题主多年前在某社区看到有人使用jmeter+selenium做UI自动化测试的时候,感觉很是诧异、怪异,为啥?众所周知在python/java+selenium+testng/pytest这样的组合框架下,为啥要选择jmeter这个东西[本身定位是接口测…

基于微信小程序的智能垃圾分类回收系统,附源码、教程

博主介绍:✌程序员徐师兄、7年大厂程序员经历。全网粉丝30W、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 1 简介 视频演示地址: 基于微信小程序的智能垃圾分类回收系统,可作为毕业设计 小…

《C++ Primer》第2章 变量(一)

参考资料: 《C Primer》第5版《C Primer 习题集》第5版 2.1 基本内置类型(P30) C 定义的基本类型包括算术类型(arithmetic type)和空类型(void),其中算术类型包括字符、整型、布尔…

postgresql-类型转换函数

postgresql-类型转换函数 简介CAST 函数to_date函数to_timestampto_charto_number隐式类型转换 简介 类型转换函数用于将数据从一种类型转换为另一种类型。 CAST 函数 CAST ( expr AS data_type )函数用于将 expr 转换为 data_type 数据类型;PostgreSQL 类型转 换…

《86盒应用于家居中控》——实现智能家居的灵动掌控

近年来,智能家居产品受到越来越多消费者的关注,其便捷、舒适的生活方式让人们对未来生活充满期待。作为智能家居方案领域的方案商,启明智显生产设计的86盒凭借出色的性能和良好的用户体验,成功应用于家居中控系统,让家…

Gof23设计模式之策略模式

1.概述 该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,…

剑指 Offer 43. 1~n 整数中 1 出现的次数(困难)

题目: class Solution { public:int countDigitOne(int n) {// mulk 表示 10^k// 在下面的代码中,可以发现 k 并没有被直接使用到(都是使用 10^k)// 但为了让代码看起来更加直观,这里保留了 klong long mulk 1;int…

Text Workflow for Mac,简单易用的文本转换工具

如果你需要一个能够将文本转换成多种语言和文件格式的工具,那么Text Workflow for Mac将是你的不二之选。 这个软件支持多种语言翻译和多种文件格式转换,让你可以轻松地将文本转换成你需要的形式。而且,它的操作非常简单,只需要几…

精讲算法的时间复杂度

目录 一、算法效率 1.算法效率 1.1如何衡量一个算法的好坏 1.2算法的复杂度 二、时间复杂度 1.时间复杂度的概念 2.大O的渐进表示法 3.常见时间复杂度的计算举例 三、空间复杂度 一、算法效率 1.算法效率 1.1如何衡量一个算法的好坏 long long Fib(int N) {if(N <…

Day6:浅谈useState

「目标」: 持续输出&#xff01;每日分享关于web前端常见知识、面试题、性能优化、新技术等方面的内容。 Day6-今日话题 谈谈react hooks中的useState &#xff0c;将从以下七个角度介绍&#xff1a; 用法 参数 返回值 作用 工作原理 优缺点 注意点 用法 useState 是一个函数&a…

Hugging News #0904: 登陆 AWS Marketplace

每一周&#xff0c;我们的同事都会向社区的成员们发布一些关于 Hugging Face 相关的更新&#xff0c;包括我们的产品和平台更新、社区活动、学习资源和内容更新、开源库和模型更新等&#xff0c;我们将其称之为「Hugging News」。本期 Hugging News 有哪些有趣的消息&#xff0…

VMware虚拟机安装CentOS7设置静态ip

vim /etc/sysconfig/network-scripts/ifcfg-ens33修改BOOTPROTO的值为static 增加最后那四项&#xff0c;参数在编辑&#xff0c;虚拟网络编辑器里面看

合宙Air724UG LuatOS-Air LVGL API控件-滑动条 (Slider)

滑动条 (Slider) 滑动条看起来和进度条是有些是有些像&#xff0c;但不同的是滑动条可以进行数值选择。 示例代码 -- 回调函数 slider_event_cb function(obj, event)if event lvgl.EVENT_VALUE_CHANGED then local val (lvgl.slider_get_value(obj) or "0")..&…

手写bind方法

<script>/** 手写bind方法* */Function.prototype.myBind function (thisArg, ...args) {return (...newArgs) > {return this.call(thisArg, ...args, ...newArgs)}}const obj {name: zs,age: 18,}function fn (a, b, c) {console.log(this)console.log(a b c)re…

《数据出境安全评估办法》解读

一、数据出境的统一评估体系正式确立 自我国《网络安全法》规定了关键信息基础设施运营者的重要数据与个人信息出境评估制度后&#xff0c;至今历时五年&#xff0c;关于数据出境及个人信息出境安全评估的立法体例、评估机制一直在探索过程中。 其后&#xff0c;国家互联网信…

PYTHON知识点学习-字典

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由 Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

java IO流(三) 转换流 打印流

转换流 前面我们学习过FileReader读取文件中的字符&#xff0c;但是FileReader默认只能读取UTF-8编码格式的文件。如果使用FileReader读取GBK格式的文件&#xff0c;可能存在乱码&#xff0c;因为FileReader遇到汉字默认是按照3个字节来读取的&#xff0c;而GBK格式的文件一个…

如何写一个sh脚本将一个本地文件通过 scp命令上传到远程的 centos服务器?

如何写一个sh脚本将一个本地文件通过 scp命令上传到远程的 centos服务器&#xff1f; 1.1 背景需求1.2 解决方案1.3 附录1.3.1 scp命令用法与示例1.3.1.1 scp命令用法与解释1.3.1.2 scp 命令用法示例1.3.1.2.1 示例一&#xff1a;从本地复制文件到远程计算机1.3.1.2.2 示例二&a…

Sqlserver 在 SELECT 语句中显示来自 GROUP BY 子句中未涉及的列

在SQL Server中&#xff0c;如果您在GROUP BY子句中对某些列进行了分组&#xff0c;但想在SELECT语句中同时显示未涉及到的其他列&#xff0c;您可以使用聚合函数和子查询的方法来实现。这可以通过在GROUP BY子查询中获取需要的聚合值&#xff0c;并在外部查询中选择其他列来完…

Java复习-20-接口(3)- 代理设计模式

代理设计模式(Proxy) 功能&#xff1a;可以帮助用户将所有的开发注意力只集中在核心业务功能的处理上。 代理模式(Proxy Pattern)是一种结构性模式。代理模式为一个对象提供了一个替身&#xff0c;以控制对这个对象的访问。即通过代理对象访问目标目标对象&#xff0c;可以在目…