应用编程之线程(二-同步篇)

线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一 致的问题。

一、为什么需要线程同步

  • 线程同步是为了对共享资源的访问进行保护。
  • 保护的目的是为了解决数据一致性的问题。
  • 出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。

二、互斥锁

互斥锁( mutex )又叫互斥量,从本质上说是一把锁,在访问共享资源之前对互斥锁进行上锁,在访问完成后释放互斥锁(解锁)。
互斥锁使用 pthread_mutex_t 数据类型表示,在使用互斥锁之前,必须首先对它进行初始化操作,可以使用两种方式对互斥锁进行初始化操作。

1、互斥锁初始化

1)使用 PTHREAD_MUTEX_INITIALIZER 宏初始化互斥锁

        pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

THREAD_MUTEX_INITIALIZER 宏已经携带了互斥锁的默认属性。

2)使用 pthread_mutex_init()函数初始化互斥锁

使用 PTHREAD_MUTEX_INITIALIZER 宏只适用于在定义的时候就直接进行初始化,对于其它情况则不能使用这种方式,譬如先定义互斥锁,后再进行初始化,或者在堆中动态分配的互斥锁,譬如使用 malloc()函数申请分配的互斥锁对象,那么在这些情况下,可以使用 pthread_mutex_init() 函数对互斥锁进行初始化。

2、互斥锁加锁和解锁

互斥锁初始化之后,处于一个未锁定状态,调用函数 pthread_mutex_lock() 可以对互斥锁加锁、获取互斥锁,而调用函数 pthread_mutex_unlock() 可以对互斥锁解锁、释放互斥锁。
调用 pthread_mutex_lock() 函数对互斥锁进行上锁,如果互斥锁处于未锁定状态,则此次调用会上锁成功,函数调用将立马返回;如果互斥锁此时已经被其它线程锁定了,那么调用 pthread_mutex_lock() 会一直 阻塞,直到该互斥锁被解锁,到那时,调用将锁定互斥锁并返回。
调用 pthread_mutex_unlock() 函数将已经处于锁定状态的互斥锁进行解锁。以下行为均属错误:
对处于未锁定状态的互斥锁进行解锁操作;
解锁由其它线程锁定的互斥锁。
如 果 有 多 个 线 程 处 于 阻 塞 状 态 等 待 互 斥 锁 被 解 锁 , 当 互 斥 锁 被 当 前 锁 定 它 的 线 程 调 用 pthread_mutex_unlock()函数解锁后,这些等待着的线程都会有机会对互斥锁上锁,但无法判断究竟哪个线程会加锁成功。

3、销毁互斥锁

当不再需要互斥锁时,应该将其销毁,通过调用 pthread_mutex_destroy() 函数来销毁互斥锁。
不能销毁还没有解锁的互斥锁,否则将会出现错误;
没有初始化的互斥锁也不能销毁。
pthread_mutex_destroy() 销毁之后的互斥锁,就不能再对它进行上锁和解锁了,需要再次调用
pthread_mutex_init() 对互斥锁进行初始化之后才能使用。

4、互斥锁死锁

如果一个线程试图对同一个互斥锁加锁两次,会出现什么情况?情况就是该线程会陷入死锁
状态,一直被阻塞永远出不来;这就是出现死锁的一种情况。
有时,一个线程需要同时访问两个或更多不同的共享资源,而每个资源又由不同的互斥锁管理。当超过一个线程对同一组互斥锁(两个或两个以上的互斥锁)进行加锁时,就有可能发生死锁;譬如,程序中使用一个以上的互斥锁,如果允许一个线程一直占有第一个互斥锁,并且在试图锁住第二个互斥锁时处于阻塞状态,但是拥有第二个互斥锁的线程也在试图锁住第一个互斥锁。因为两个线程都在相互请求另一个线程拥有的资源,所以这两个线程都无法向前运行,会被一直阻塞,于是就产生了死锁。
要避免此类死锁的问题,最简单的方式就是定义互斥锁的层级关系,当多个线程对一组互斥锁操作时,总是应该按照相同的顺序对该组互斥锁进行锁定。

三、条件变量

条件变量是线程可用的另一种同步机制。条件变量用于自动阻塞线程,知道某个特定事件发生或某个条件满足为止,通常情况下,条件变量是和互斥锁一起搭配使用的。
使用条件变量主要包括两个动作:
一个线程等待某个条件满足而被阻塞;
另一个线程中,条件满足时发出“信号”。
条件变量通常搭配互斥锁来使用,是因为条件的检测是在互斥锁的保护下进行的,也就是说条件本身是由互斥锁保护的,线程在改变条件状态之前必须首先锁住互斥锁,不然就可能引发线程不安全的问题。

1、条件变量初始化

条件变量使用 pthread_cond_t 数据类型来表示,类似于互斥锁,在使用条件变量之前必须对其进行初始 化。初始化方式同样也有两种:使用宏 PTHREAD_COND_INITIALIZER 或者使用函数 pthread_cond_init() , 使用宏的初始化方法与互斥锁的初始化宏一样
对于初始化与销毁操作,有以下问题需要注意:
在使用条件变量之前必须对条件变量进行初始化操作,使用 PTHREAD_COND_INITIALIZER 宏或者函数 pthread_cond_init() 都行;
对已经初始化的条件变量再次进行初始化,将可能会导致未定义行为;
对没有进行初始化的条件变量进行销毁,也将可能会导致未定义行为;
对某个条件变量而言,仅当没有任何线程等待它时,将其销毁才是最安全的;
pthread_cond_destroy() 销毁的条件变量,可以再次调用 pthread_cond_init() 对其进行重新初始化

2、通知和等待条件变量

条件变量的主要操作便是发送信号(signal )和等待。发送信号操作即是通知一个或多个处于等待状态的线程,某个共享变量的状态已经改变,这些处于等待状态的线程收到通知之后便会被唤醒,唤醒之后再检查条件是否满足。等待操作是指在收到一个通知前一直处于阻塞状态。
函数 pthread_cond_signal() pthread_cond_broadcast() 均可向指定的条件变量发送信号,通知一个或多个处于等待状态的线程。调用 pthread_cond_wait()函数是线程阻塞,直到收到条件变量的通知。当程序当中使用条件变量,当判断某个条件不满足时,调用 pthread_cond_wait()函数将线程设置为等待状态(阻塞)。
注意注意的是,条件变量并不保存状态信息,只是传递应用程序状态信息的一种通讯机制。如果调用pthread_cond_signal()和 pthread_cond_broadcast() 向指定条件变量发送信号时,若无任何线程等待该条件变量, 这个信号也就会不了了之。
当调用 pthread_cond_broadcast() 同时唤醒所有线程时,互斥锁也只能被某一线程锁住,其它线程获取锁失败又会陷入阻塞。
综上,当线程锁住互斥锁之后,线程内会判断此时判定条件是否符合,不符合就休眠并解锁,当收到其他线程信号时会被唤醒,再次判断条件是否满足,满足就执行接下来的语句,不满足继续休眠。

3、条件变量的判断条件

使用条件变量,都会有与之相关的判断条件,通常情况下,会涉及到一个或多个共享变量。

判断条件必须使用 while 循环,而不是 if 语句,这是一种通用的设计原则:当线程从 pthread_cond_wait()返回时, 并不能确定判断条件的状态,应该立即重新检查判断条件,如果条件不满足,那就继续休眠等待。
pthread_cond_wait() 返回后,并不能确定判断条件是真还是假,其理由如下:
当有多于一个线程在等待条件变量时,任何线程都有可能会率先醒来获取互斥锁,率先醒来获取到 互斥锁的线程可能会对共享变量进行修改,进而改变判断条件的状态。
可能会发出虚假的通知。

四、自旋锁

自旋锁与互斥锁很相似,从本质上说也是一把锁,在访问共享资源之前对自旋锁进行上锁,在访问完成 后释放自旋锁(解锁);事实上,从实现方式上来说,互斥锁是基于自旋锁来实现的,所以自旋锁相较于互斥锁更加底层。

如果在获取自旋锁时,自旋锁处于未锁定状态,那么将立即获得锁(对自旋锁上锁);如果在获取自旋锁时,自旋锁已经处于锁定状态了,那么获取锁操作将会在原地“自旋”,直到该自旋锁的持有者释放了锁。
由此介绍可知,自旋锁与互斥锁相似,但是互斥锁在无法获取到锁时会让线程陷入阻塞等待状态;而自旋锁在无法获取到锁时,将会在原地“自旋”等待。“自旋”其实就是调用者一直在循环查看该自旋锁的持有者 是否已经释放了锁,“自旋”一词因此得名。
自旋锁的不足之处在于:自旋锁一直占用的 CPU ,它在未获得锁的情况下,一直处于运行状态(自旋), 所以占着 CPU ,如果不能在很短的时间内获取锁,这无疑会使 CPU 效率降低。
试图对同一自旋锁加锁两次必然会导致死锁,而试图对同一互斥锁加锁两次不一定会导致死锁,原因在于互斥锁有不同的类型,当设置为 PTHREAD_MUTEX_ERRORCHECK 类型时,会进行错误检查,第二次加锁会返回错误,所以不会进入死锁状态。
因此我们要谨慎使用自旋锁,自旋锁通常用于以下情况:需要保护的代码段执行时间很短,这样就会使 得持有锁的线程会很快释放锁,而“自旋”等待的线程也只需等待很短的时间;在这种情况下就比较适合使用自旋锁,效率高!
综上所述,再来总结下自旋锁与互斥锁之间的区别:
实现方式上的区别:互斥锁是基于自旋锁而实现的,所以自旋锁相较于互斥锁更加底层;
开销上的区别:获取不到互斥锁会陷入阻塞状态(休眠),直到获取到锁时被唤醒;而获取不到自 旋锁会在原地“自旋”,直到获取到锁;休眠与唤醒开销是很大的,所以互斥锁的开销要远高于自 旋锁、自旋锁的效率远高于互斥锁;但如果长时间的“自旋”等待,会使得 CPU 使用效率降低,
故自旋锁不适用于等待时间比较长的情况。(举例来说,当自旋锁发现锁被别人锁住了,它就会一直在旁边等这把锁开,互斥锁发现锁被锁住后会找地方休眠并等着别的函数发信号来唤醒它
使用场景的区别:自旋锁在用户态应用程序中使用的比较少,通常在内核代码中使用比较多;因为自旋锁可以在中断服务函数中使用,而互斥锁则不行,在执行中断服务函数时要求不能休眠、不能被抢占(内核中使用自旋锁会自动禁止抢占),一旦休眠意味着执行中断服务函数时主动交出了CPU 使用权,休眠结束时无法返回到中断服务函数中,这样就会导致死锁!

五、读写锁

互斥锁或自旋锁要么是加锁状态、要么是不加锁状态,而且一次只有一个线程可以对其加锁。读写锁有 3 种状态:读模式下的加锁状态(以下简称读加锁状态)、写模式下的加锁状态(以下简称写加锁状态)和不加锁状态。
一次只有一个线程可以占有写模式的读写锁,但是可以有多个线程同时占有读模式的读写锁。因此可知,读写锁比互斥锁具有更高的并行性!
读写锁有如下两个规则:
当读写锁处于写加锁状态时,在这个锁被解锁之前,所有试图对这个锁进行加锁操作(不管是以读模式加锁还是以写模式加锁)的线程都会被阻塞。
当读写锁处于读加锁状态时,所有试图以读模式对它进行加锁的线程都可以加锁成功;但是任何以写模式对它进行加锁的线程都会被阻塞,直到所有持有读模式锁的线程释放它们的锁为止。
读写锁也叫做共享互斥锁。当读写锁是读模式锁住时,就可以说成是共享模式锁住。当它是写模式锁住时,就可以说成是互斥模式锁住。

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

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

相关文章

用Cmake编译程序时,链接到FFmpeg库

用Cmake编译程序时,链接到FFmpeg库 一、前言 可喜可贺,折腾了一晚上终于把这个勾八链接成功了,已经要吐了。看到下面控制台的输出,吾心甚慰呀😭 [100%] Linking CXX executable rknn_yolov5_demo [100%] Built targe…

SpringBoot 操作 Redis

导入对应版本的依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>修改配置文件中的信息 spring:redis:host: 127.0.0.1port: 8888注意: 我这里 xsh…

【python】深度解剖!一文搞懂字符串常用功能

【python】深度解剖!一文搞懂字符串常用功能 【先赞后看养成习惯】求点赞+关注+收藏😀 目录 【python】深度解剖!一文搞懂字符串常用功能字符串的创建:字符串的格式化——占位符.format方法字符串的分割字符串的连接字符串的重复字符串的大小写转换字符串的判断字符串的…

sherpa + ncnn 离线语音识别

目录结构 前言音视频格式转为wavsherpa-ncnn编译LinuxWindowswindows编译中遇到的问题问题“nmake -? failed with: no such file or directory”编译失败原因 成功编译截图 可执行程序说明模型下载语言识别测试LinuxWindows 参考文献 前言 小编需要实现离线音视频语言部分识…

C++ 程序的内存分配

C 程序的内存分配 C 程序的内存分配栈堆数据区程序代码区参考 C 程序的内存分配 一个 C 编译的程序占用内存分为以下几个部分&#xff08;从高地址到低地址&#xff09;&#xff1a; 内核空间&#xff1a;由操作系统创建并控制&#xff0c;用户代码不能读写。栈&#xff1a;由…

mysql 日环比 统计

接到一个任务&#xff0c;要计算日环比的情况。 16、查询销售额日环比情况 日环比&#xff1a; &#xff08;今日-昨日&#xff09;/ 昨日 的一个比率情况。 1&#xff0c;建表 DROP TABLE IF EXISTS sale; create table sale(id int not null AUTO_INCREMENT,record_date da…

IDEA插件:CodeGeex

前言 CodeGeeX是由清华大学和智谱AI联合开发的多语言代码生成模型。CodeGeeX是一款AI编程助手&#xff0c;其功能类似于Github Copilot、Codeium、CodeWhisperer、Bito等智能编程助手。CodeGeeX支持Python、C、Java、JavaScript、Go等10多种主流编程语言。它可以帮助程…

windows驱动开发-WDM框架(一)

在前面的文章中解释过&#xff0c;NT5.0之后windows确定了新的架构Windows Driver Model (WDM)&#xff0c;在Vista之后又推出了Windows Driver Framework(WDF)&#xff0c;这两个都属于驱动程序框架&#xff0c;那么它们的之间的关系是怎样的&#xff1f; WDF是对WDM进行的封…

天线基本原理;光纤传播信号原理;光纤怎么表示0,1的

目录 天线基本原理 电话,收音机,4G,5G 三大运营商频段划分 光纤传播信号原理

利用大语言模型,矢量数据库实现数据库的智能搜索

目的 数据库使用SQL 语言查询数据&#xff0c;数据库的记录中要有一个关键字段&#xff08;通常称为主键字段&#xff0c;它的值在数据库列表中是唯一的&#xff09;,数据记录是结构化的. 如果你需要根据数据记录的内容来查询数据记录&#xff0c;就需要通过Select 语句在数据库…

OpenCV杂记(1):绘制OSD(cv::getTextSize, cv::putText)

1. 简述 我们使用OpenCV时&#xff0c;有时会在图像的某个位置绘制OSD信息&#xff0c;如绘制一些字符串作为指示信息。 本文将简要介绍在图像&#xff08;cv::Mat&#xff09;上绘制固定的字符串信息。 2. 使用的API &#xff08;1&#xff09;cv::getTextSize() CV_EXPORT…

vue3 删除对象中的属性,可以使用js里的delete,但需注意ts定义对象类型!

如上如&#xff0c;当使用delete 删除stateData中的属性时&#xff0c; 报错&#xff0c;意思为 TypeScript 错误“‘delete’ 运算符的操作数必须是可选的 什么原因呢&#xff1f;是因为我偷懒 缺少了ts定义类型 方法一&#xff1a; &#xff08;不推荐&#xff09; delete …

MultiHeadAttention在Tensorflow中的实现原理

前言 通过这篇文章&#xff0c;你可以学习到Tensorflow实现MultiHeadAttention的底层原理。 一、MultiHeadAttention的本质内涵 1.Self_Atention机制 MultiHeadAttention是Self_Atention的多头堆嵌&#xff0c;有必要对Self_Atention机制进行一次深入浅出的理解&#xff0c;这…

Linux Makefile用法

1、什么是makefile&#xff1f; Makefile&#xff1a;将不同模块放在不同的目录中&#xff0c;定义一系列的规则进行 “自动化编译”2、Makefile写法 vim makefile 填写样例&#xff1a; app:sub.c add.c mult.c div.c main.cgcc sub.c add.c mult.c div.c main.c -o app3、工作…

刷代码随想录有感(39):每层最大值

题干&#xff1a; 代码&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), …

OpenCV基本图像处理操作(十一)——图像特征Sift算法

图像尺度空间 在一定的范围内&#xff0c;无论物体是大还是小&#xff0c;人眼都可以分辨出来&#xff0c;然而计算机要有相同的能力却很难&#xff0c;所以要让机器能够对物体在不同尺度下有一个统一的认知&#xff0c;就需要考虑图像在不同的尺度下都存在的特点。 尺度空间的…

《6G数据面架构研究》

目录 一、数据服务的定义二、6G数据服务驱动力及面临的挑战6G数据服务的业务驱动6G数据服务的技术驱动6G数据服务的网络内在驱动6G数据面面临的挑战 三、6G数据服务典型场景自动化网络运维用户体验提升通信感知数据服务 四、6G数据面架构研究数据面架构视图功能定义说明&#x…

kafka部分partition的leader=-1修复方案整理

kafka部分partition的leader-1修复方案整理 1. 背景说明2. 修复测试2.1 创建正常的topic并验证生产和消费2.2 停止kafka模拟leader-12.3 修复parition2.4 修复完成验证生产消费是否恢复 3. 疑问和思考3.1 kafka在进行数据消费时&#xff0c;如果有partition的leader-1&#xff…

hive 不同维度聚合 grouping sets 使用详情

当结构数据表中&#xff0c;多列维度字段场景&#xff0c; 需要看不同维度聚合后的数据集时。 整体 demo sql select if(CAST (GROUPING__ID AS INT) & 80, A, all) as A,if(CAST (GROUPING__ID AS INT) & 40, B, all) as B,C,D from table_name where dt ${dt} gro…

从迷宫问题理解dfs

文章目录 迷宫问题打印路径1思路定义一个结构体要保存所走的路径&#xff0c;就需要使用到栈遍历所有的可能性核心代码 部分函数递归图源代码 迷宫问题返回最短路径这里的思想同上面类似。源代码 迷宫问题打印路径1 定义一个二维数组 N*M &#xff0c;如 5 5 数组下所示&…