Linux_线程的同步与互斥

目录

1、互斥相关概念 

2、代码体现互斥重要性

3、互斥锁 

3.1 初始化锁 

3.2 申请、释放锁 

3.3 加锁的思想

3.4 实现加锁 

3.5 锁的原子性 

4、线程安全 

4.1 可重入函数 

4.2 死锁 

5、线程同步 

5.1 条件变量初始化 

5.2 条件变量等待队列

5.3 唤醒等待队列

5.4 实现线程同步

结语 


前言:

        在Linux下,线程是一个很重要的概念,他可以提高多执行流的并发度,而同步与互斥是对线程的一种约束行为,比如当多个线程都访问同一个资源时,若不对该资源加以保护则会导致意料之外的错误。具体的保护措施是让线程访问共享资源时具有互斥性,即当一个线程访问时别的线程无法访问,通常用互斥锁来实现。而同步是为了让多个线程具有一定的顺序来访问共享内存,保障每个线程访问资源的机会是一样的。

1、互斥相关概念 

        线程之所以需要互斥,是因为多线程在访问共享资源时,可能该资源只允许被修改一次,但是其他线程在修改的时候“刹不住车”,导致该资源被修改多次,原因就是多个线程同时访问了该资源,如下图所示:

        在概念层面上,通常把共享资源叫做临界资源。在代码层面,把访问共享资源的代码叫做临界区


        当线程有了互斥约束后,就不会出现上述a=0时继续访问a的情况,如下图:

2、代码体现互斥重要性

        在实际生活中,某些有限的物品是不能出现负数的情况的,比如抢票,票为0时是不能继续抢票的,但是当实现多线程抢票时,若没有互斥的约束,则很容易发生票为0时还在抢票,模拟抢票的代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 1000; // 用多线程模拟抢票class threadData
{
public:threadData(int number){threadname = "线程-" + to_string(number);}public:string threadname;
};void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (true){if(tickets > 0){usleep(1000);printf("%s, 抢到一张票: %d\n", name, tickets); tickets--;}elsebreak;}printf("%s 退出\n", name);return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= 4; i++){pthread_t tid;threadData *td = new threadData(i);thread_datas.push_back(td);pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}//等待线程for (auto thread : tids){pthread_join(thread, nullptr);}//释放空间资源for (auto td : thread_datas){delete td;}return 0;
}

         运行结果:

        从结果可以看到,发生了负数票的情况,原因就是上面多线程代码没有任何互斥的约束。


        对上面代码进行分析找出其临界区,全局变量ticket是临界资源,因此代码中对ticket的访问就是临界区,如下图所示:

        为了解决上面的问题,只能使用互斥约束多线程,而互斥就必须用到互斥锁。 

3、互斥锁 

         实现互斥锁的步骤:

        1、创建一个锁变量。

        2、使用接口初始化该变量。

        3、在临界区处申请该锁。

        4、临界区代码执行完后释放锁。

        5、销毁锁。

        值得注意的是:只能用一把锁限制对临界区的访问,即线程要想访问临界区,则必须申请到该锁才能访问,没有申请到锁的线程就无法访问临界区。 


        申请锁的示意图如下:

3.1 初始化锁 

        初始化锁用到的接口介绍如下:

#include <pthread.h>
//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);//初始化锁,方式1
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
//restrict mutex表示要初始化的锁
//restrict attr表示初始化的属性//初始化锁,方式2
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//定义在全局,则mutex锁就已经被初始化了

        pthead_mutex_t是库提供的数据类型,用于定义一个锁。方式2是一个全局变量初始化锁,若用方式2初始化一个锁则无需对该锁进行destroy。注意:若用方式2进行锁的初始化则该锁必须是全局的。

3.2 申请、释放锁 

        锁的初始化工作完成后,接下来就是申请锁,申请锁的接口介绍如下:

#include <pthread.h>int pthread_mutex_lock(pthread_mutex_t *mutex);//申请mutex锁int pthread_mutex_trylock(pthread_mutex_t *mutex);//申请mutex锁int pthread_mutex_unlock(pthread_mutex_t *mutex);//释放mutex锁

        pthread_mutex_lock申请不到锁会阻塞在该函数处,而pthread_mutex_trylock申请不到锁不会阻塞,会继续执行下面代码。

3.3 加锁的思想

        申请锁就是加锁,加锁的本质是用时间换来线程安全,让线程访问临界资源时串开访问,对临界区进行加锁时尽量缩小临界区的代码量,因为临界区的代码越少,执行的速度越快,则进程被cpu挂起的概念就越低,被cpu挂起的概念低了则可以减少其他线程等的时间,因为当申请到锁的线程被挂起了,那么其他的线程就算被cpu调度了也不能执行临界区的代码(因为其他线程没有持有锁),只能干等。

3.4 实现加锁 

        对上述代码实现加锁,代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 1000; // 用多线程模拟抢票class threadData
{
public:threadData(int number , pthread_mutex_t *mutex){threadname = "线程-" + to_string(number);lock = mutex;}public:string threadname;pthread_mutex_t *lock;
};void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (true){pthread_mutex_lock(td->lock); //申请锁if(tickets > 0){usleep(1000);printf("%s, 抢到一张票: %d\n", name, tickets);tickets--;pthread_mutex_unlock(td->lock);//释放锁}else{pthread_mutex_unlock(td->lock);//释放锁break;}//usleep(12); //先把此处的usleep屏蔽,观察抢票现象}printf("%s 退出\n", name);return nullptr;
}int main()
{pthread_mutex_t lock;//定义一个锁pthread_mutex_init(&lock, nullptr);//对该锁进行初始化vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= 3; i++){pthread_t tid;threadData *td = new threadData(i ,&lock);thread_datas.push_back(td);//要将锁也传给线程pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}//等待线程for (auto thread : tids){pthread_join(thread, nullptr);}//释放空间资源for (auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock);return 0;
}

         运行结果:

        从结果看,虽然没有出现负票的情况,但是发现只有一个线程在抢票,原因很简单,肯定是只有该进程申请到锁了,其他线程没申请到,那么为什么只有该线程能申请到,而其他线程申请不到呢?是因为这个线程刚释放完锁后他就立马再进行申请锁的动作了,他之所以可以比其他线程更快申请到锁的原因是“他离锁最近”,具体示意图如下:


        所以在一个线程释放锁后,可以手动对该线程进行sleep,让其他线程有机会去申请到锁,因此把上述代码中释放锁后面的usleep放开,就可以让其他线程申请到锁了,运行结果如下:

3.5 锁的原子性 

        从上文可以得知,当多线程访问共享资源时,若没有互斥约束,则会发生错误,所以对线程进行加锁的操作,但是锁本身也是共享资源,因为多线程都能看到锁并且申请他,那么申请锁的时候不好导致同样的问题吗?

        答案是不会,多线程访问共享资源之所以会发生意料之外的错误,是因为多线程对共享资源做修改操作的时候,这些修改操作在底层被转换成汇编语句,虽然上层看到的修改操作只有一句代码,但是在底层转换成两三句汇编指令,而cpu一次只能运算一句汇编指令,这就导致同一个操作没有真正被cpu执行完就被切换走了,等到下次继续执行该操作时,从内存中读取的数据可能已经被别的线程修改了,这就导致了意料之外的错误。而申请锁的动作只有一句汇编指令,他的状态只有两种:1、要么没申请到锁,2、要么申请到锁。不存在执行一半被切走的可能,通常把这种状态叫做原子性,因此锁是具有原子性的。

4、线程安全 

        线程安全指的是在多线程的并行下,访问某些资源时,不会导致该资源的数据损坏或出现意料之外的错误,线程与线程之间不会互相干扰对方的操作,多线程能够安全的执行下去,把这叫做线程安全。

4.1 可重入函数 

        可重入函数值得是当同一个函数被多个线程调用时,调用的结果不会产生任何的问题,比如不会导致数据损坏或者资源泄漏,则该函数被称为可重入函数,否则,是不可重入函数。

4.2 死锁 

         死锁指的是当线程申请锁时造成了循环申请,也就是说线程1要申请线程2的锁,而线程2要申请线程1的锁,造成死循环称之为死锁,具体示意图如下:

        造成死锁的四个必要条件:

1、互斥条件:一把锁只能被一个线程申请。
2、请求与保持条件:多线程之间互相申请对方的锁,但是对方就是不释放该锁。
3、不剥夺条件 :不释放对方的锁,即使要申请的锁在对方手里也不主动释放。
4、循环等待条件 : 多线程循环等待彼此的资源。

        只要不满足上面4个条件是任何一个,则就造成不了死锁。 

5、线程同步 

        线程同步的目的是让每个线程申请锁的能力是有顺序性的,即每个线程都可以公平的申请到锁,通常是定义一个条件变量,然后将线程放入等待队列中(申请的前提是该线程必须持有锁),申请到锁的线程就能够进入等待队列中等待了,进入等待队列时线程会自动释放锁,目的是让下一个线程申请锁然后也入队,因此条件变量必须搭配锁才能使用。

        将线程放入等待队列的示意图如下:


        唤醒等待队列里的线程去申请锁:

        等待队列申请锁的逻辑:首先需要唤醒该等待队列,然后队列里的第一个线程可以重新去申请锁,访问临界资源结束后,释放锁的线程会回到队列的末尾,如此逻辑就能够实现线程同步了

5.1 条件变量初始化 

        条件变量的初始化逻辑和锁的初始化逻辑相似,都是有两种初始化方式,具体接口如下:

#include <pthread.h>
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);//条件变量初始化方式1
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//restrict cond表示要初始化的条件变量的地址
//attr表示条件变量初始化的属性设置//条件变量初始化方式2
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//在全局定义完成初始化

5.2 条件变量等待队列

        在条件变量完成初始化后,需要将线程放入条件变量的等待队列中, 这个过程只需要调用函数pthread_cond_wait即可完成,但是要注意调用该函数时当前线程必须是持有锁的,所以使用条件变量必须依赖锁,pthread_cond_wait函数介绍如下:

#include <pthread.h>int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
//cond表示将该线程放入哪个条件变量的队列
//mutex表示等待队列被唤醒后可申请的锁

        当线程调用此函数时,会释放已经申请的锁然后在等待队列中排队,所以线程的执行流会阻塞在该函数处。

5.3 唤醒等待队列

        当调用,唤醒函数介绍如下:

#include <pthread.h>int pthread_cond_broadcast(pthread_cond_t *cond);
//cond表示要唤醒的条件变量等待队列,该函数被调用一次则整个队列都被唤醒int pthread_cond_signal(pthread_cond_t *cond);
//cond表示要唤醒的条件变量等待队列,该函数被调用一次则只唤醒队头的线程

5.4 实现线程同步

         上文中抢票代码的逻辑是线程释放锁后对该线程进行sleep,这么做让其他线程有了申请锁的机会,其实这也是同步的一种方法,只不过sleep的时间不好控制,而现在我们无需对线程进行sleep也可以实现同步,即使用条件变量进行同步,让系统去维护同步机制,可以更好的控制同步。

        实现线程同步的代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 1000; // 用多线程模拟抢票
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//全局初始化class threadData
{
public:threadData(int number , pthread_mutex_t *mutex){threadname = "线程-" + to_string(number);lock = mutex;}public:string threadname;pthread_mutex_t *lock;
};void *getTicket(void *args)
{threadData *td = static_cast<threadData *>(args);const char *name = td->threadname.c_str();while (true){pthread_mutex_lock(td->lock); //申请锁pthread_cond_wait(&cond,td->lock);//将线程放入等待队列if(tickets > 0){//usleep(1000);printf("%s, 抢到一张票: %d\n", name, tickets);tickets--;pthread_mutex_unlock(td->lock);//释放锁}else{pthread_mutex_unlock(td->lock);//释放锁break;}}printf("%s 退出\n", name);return nullptr;
}int main()
{pthread_mutex_t lock;//定义一个锁pthread_mutex_init(&lock, nullptr);//对该锁进行初始化vector<pthread_t> tids;vector<threadData *> thread_datas;for (int i = 1; i <= 3; i++)//创建3个线程{pthread_t tid;threadData *td = new threadData(i ,&lock);thread_datas.push_back(td);//要将锁也传给线程pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);tids.push_back(tid);}sleep(2);//目的是让线程全部都放入队列中,然后再进行唤醒//唤醒队列while(true){pthread_cond_signal(&cond);}//等待线程for (auto thread : tids){pthread_join(thread, nullptr);}//释放空间资源for (auto td : thread_datas){delete td;}pthread_mutex_destroy(&lock);return 0;
}

        运行结果:

        从结果可以看到,没有出现负票的情况,并且所有线程都在抢票。这里注意pthread_mutex_lock和pthread_cond_wait两个函数对锁的申请和释放逻辑,调用pthread_mutex_lock时线程会申请锁,然后调用pthread_cond_wait时,线程会释放锁,并且阻塞在该函数处等待被唤醒,被唤醒后该线程又重新申请锁,申请成功后执行临界区代码。

结语 

        以上就是关于线程的同步与互斥讲解,若使用多线程进行并发式的执行程序,那么同步和互斥是必不可少的保护措施,他保障了多线程并发执行时线程的安全,防止出现意料之外的错误,因此对临界资源进行同步和互斥是多线程执行时非常重要的一步。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!

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

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

相关文章

视觉网络的有效调制

摘要 链接&#xff1a;https://arxiv.org/pdf/2403.19963 在这项工作中&#xff0c;我们提出了高效调制&#xff08;EfficientMod&#xff09;&#xff0c;这是一种用于高效视觉网络的新型设计。我们重新审视了调制机制&#xff0c;该机制通过卷积上下文建模和特征投影层处理输…

Java并发(三)Java并发核心机制

J.U.C 简介 Java 的 java.util.concurrent 包&#xff08;简称 J.U.C&#xff09;中提供了大量并发工具类&#xff0c;是 Java 并发能力的主要体现&#xff08;注意&#xff0c;不是全部&#xff0c;有部分并发能力的支持在其他包中&#xff09;。从功能上&#xff0c;大致可以…

Spring Boot 日志 (初级)

什么是日志呢&#xff1f;其实就是一条条的打印语句&#xff0c;我们就可以根据打印出来的日志&#xff0c;去分析程序存在的问题等。虽然作为后端开发人员&#xff0c;日志并不是那么重要&#xff0c;但是在学习的过程中&#xff0c;也是比较重要的&#xff0c;可以使用在我们…

[Spring] Spring配置文件

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏: &#x1f9ca; Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm1001.2014.3001.5482 &#x1f355; Collection与…

主流大数据调度工具DolphinScheduler之数据采集

今天继续给大家分享主流大数据调度工具DolphinScheduler&#xff0c;以及数据的ETL流程。 一&#xff1a;调度工具DS 主流大数据调度工具DolphinScheduler&#xff0c; 其定位&#xff1a;解决数据处理流程中错综复杂的依赖关系 任务支持类型&#xff1a;支持传统的shell任…

数据结构之字符串的最长公共子序列问题详解与示例(C,C++)

文章目录 1、最长公共子序列定义2、动态规划解法3、状态转移方程初始化构建最长公共子序列 4、C 和 C 实现示例C 语言实现C 语言实现 5、总结 字符串的最长公共子序列&#xff08;Longest Common Subsequence, LCS&#xff09;是计算机科学中的一个经典问题&#xff0c;属于动态…

接口开发:Orcal数据库的批量新增sql

场景&#xff1a;在日常的CURD中一定会用到批量新增。在我们的项目中&#xff0c;使用的数据库是Orcal&#xff0c;由于之前基本都是使用Mysql的&#xff0c;使用的sql语句也基本都是用mysql的。但是在这次的接口编写时用mysql的批量新增出了问题&#xff0c;刚开始我还以为是写…

[MySQL]02 存储引擎与索引,锁机制,SQL优化

Mysql存储引擎 可插拔式存储引擎 索引是在存储引擎底层上实现的 inno DB MySQL默认存储引擎: inno DB高可靠性和高性能的存储引擎 DML操作遵循ACID模型支持事务行级锁,提高并发访问性能支持外键 约束,保证数据完整性和可靠性 MySAM MySAM是MySQL的早期引擎 特点: 不支持事…

安装 VMware vSphere vCenter 8.0

安装 VMware vSphere vCenter 8.0 1、运行安装程序 2、语言选择中文 3、点下一步 4、接受许可协议&#xff0c;点下一步 5、填写部署vCenter服务的ESXI主机IP地址以及对应ESXI主机的账号密码&#xff0c;这里将vCenter服务部署在192.168.1.14这台ESXi主机上 6、接受证书警告 7…

自动化测试中如何应对网页弹窗的挑战!

在自动化测试中&#xff0c;网页弹窗的出现常常成为测试流程中的一个难点。无论是警告框、确认框、提示框&#xff0c;还是更复杂的模态对话框&#xff0c;都可能中断测试脚本的正常执行&#xff0c;导致测试结果的不确定性。本文将探讨几种有效的方法来应对网页弹窗的挑战&…

21k star 开源项目,让模糊图像秒变高清!

不知道大家有没有遇到这样的需求&#xff1a;电脑里有一些很久之前保存的好看的图片&#xff0c;但是因为分辨率不高&#xff0c;当做壁纸使用会变得模糊。或者是从网上下载的需要使用的图片&#xff0c;找不到更高清的版本导致使用效果不好。之前分享过基于 Real-ESRGAN 算法修…

Redis中的哨兵(Sentinel)

上篇文章我们讲述了Redis中的主从复制&#xff08;Redis分布式系统中的主从复制-CSDN博客&#xff09;&#xff0c;本篇文章针对主从复制中的问题引出Redis中的哨兵&#xff0c;希望本篇文章会对你有所帮助。 文章目录 一、引入哨兵机制 二、基本概念 三、主从复制的问题 四、哨…

php相关

php相关 ​ 借鉴了小迪安全以及各位大佬的博客&#xff0c;如果一切顺利&#xff0c;会不定期更新。 如果感觉不妥&#xff0c;可以私信删除。 默认有php基础。 文章目录 php相关1. php 缺陷函数1. 与2. MD53. intval()4. preg_match() 2. php特性1. php字符串解析特性2. 杂…

Cisco 路由重发布 —— 实现路由信息在不同路由域间的传递

一、技术背景 在实际的组网中&#xff0c;可能会遇到这样一个场景&#xff1a;在一个网络中同时存在两种或者两种以上的路由协议。例如客户的网络原先是纯 Cisco 的设备&#xff0c;使用 EIGRP 协议将网络的路由打通。但是后来网络扩容&#xff0c;增加了一批华为的设备&#…

【题解 Kruskal重构树 LCA】 星际导航

星际导航 分析&#xff1a; 这也是一个比较老的题目了 今天突然想学一下kruskal重构树&#xff0c;就做到了这个题。 首先我们要明白&#xff0c;为什么这道题的路径一定是在最小生成树里&#xff1f; 或许是我们惯有的经验&#xff1a;最小的最大或者最大的最小无非两种套路…

信号【Linux】

文章目录 信号处理方式&#xff08;信号递达&#xff09;前后台进程 终端按键产生信号kill系统调用接口向进程发信号阻塞信号sigset_tsigprocmasksigpending内核态与用户态&#xff1a;内核空间与用户空间内核如何实现信号的捕捉 1、信号就算没有产生&#xff0c;进程也必须识别…

【C++】C++类和对象详解(上)

目录 思维导图大纲&#xff1a; 思维方面&#xff1a; 1. 类的定义&#xff1a; 2. 类的特点&#xff1a; 3. this指针&#xff1a; 4. 类的默认成员函数 默认构造函数 1.构造函数 2.析构函数 3.拷贝构造函数 4. 赋值运算符重载 1. 运算符重载 5. 日期类实现&#…

【CPP】CPP的内存管理

目录 10 C/C内存管理10.1 内存分布10.2 C的动态内存管理10.3 C的内存管理10.4 new失败的检测10.5 operator new与operator delete函数10.5 new与malloc()的区别,delete与free()的区别10.6 定位new表达式 这里是oldking呐呐,感谢阅读口牙!先赞后看,养成习惯! 个人主页:oldking呐…

编程中的智慧四:设计模式总览

前面三篇我们通过从一些零散的例子&#xff0c;和简单应用来模糊的感受了下设计模式在编程中的智慧&#xff0c;从现在开始正式进入设计模式介绍&#xff0c;本篇将从设计模式的7大原则、设计模式的三大类型、与23种设计模式的进行总结&#xff0c;和描述具体意义。 设计模式体…

使用Diffusion Models进行街景视频生成

Diffusion Models专栏文章汇总&#xff1a;入门与实战 前言&#xff1a;街景图生成相当有挑战性&#xff0c;目前的文本到视频的方法仅限于生成有限范围的场景的短视频&#xff0c;文本到3D的方法可以生成单独的对象但不是整个城市。除此之外街景图对一致性的要求相当高&#x…