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.rhkb.cn/news/381661.html

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

相关文章

【openGL】阴影映射问题:shadow acne(阴影失真)和peter panning(阴影悬浮)

【openGL】阴影映射问题&#xff1a;shadow acne&#xff08;阴影失真&#xff09;和peter panning&#xff08;阴影悬浮&#xff09; 1.《Learn Opengl》第35章中提到&#xff0c;一般在按照光线投射产生深度贴图的做法做阴影映射时&#xff0c;会出现摩尔纹&#xff0c;这种…

浏览器插件利器--allWebPluginV2.0.0.16-beta版发布

allWebPlugin简介 allWebPlugin中间件是一款为用户提供安全、可靠、便捷的浏览器插件服务的中间件产品&#xff0c;致力于将浏览器插件重新应用到所有浏览器。它将现有ActiveX控件直接嵌入浏览器&#xff0c;实现插件加载、界面显示、接口调用、事件回调等。支持Chrome、Firefo…

AI 模型本地推理 - YYPOLOE - Python - Windows - GPU - 吸烟检测(目标检测)- 有配套资源直接上手实现

Python 运行 - GPU 推理 - windows 环境准备python 代码 环境准备 FastDeploy预编译库下载 conda config --add channels conda-forge && conda install cudatoolkit11.2 cudnn8.2 pip install fastdeploy_gpu_python-0.0.0-cp38-cp38-win_amd64.whlpython 代码 impo…

电路学习——经典运放电路之滞回比较器(施密特触发器)(2024.07.18)

参考链接1: 电子设计教程29&#xff1a;滞回比较器&#xff08;施密特触发器&#xff09; 参考链接2: 滞回比较器电路详细分析 参考链接3: 比较器精髓&#xff1a;施密特触发器&#xff0c;正反馈的妙用 参考链接4: 比较器反馈电阻选多大&#xff1f;理解滞后效应&#xff0c;轻…

Vue3 内置组件Teleport以及Susponse

1、Teleport 1.1 概念 将组件模版中的指定的dom挂载&#xff08;传送&#xff09;到指定的dom元素上&#xff0c;如挂载到body中&#xff0c;挂载到#app选择器上面。 1.2 应用场景 经典案例如&#xff1a;模态框。 <template><teleport to"body">&l…

【LeetCode】对称二叉树

目录 一、题目二、解法完整代码 一、题目 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true 示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&#…

如何在 Android 中删除和恢复照片

对于智能手机用户来说&#xff0c;相机几乎已经成为一种条件反射&#xff1a;你看到值得注意的东西&#xff0c;就拍下来&#xff0c;然后永远保留这段记忆。但如果那张照片不值得永远保留怎么办&#xff1f;众所周知&#xff0c;纸质快照拿在手里很难舍弃&#xff0c;而 Andro…

『 Linux 』信号概念与信号的产生

文章目录 信号概念前台进程与后台进程信号的本质硬件理解信号的产生 信号概念 "信号"一词指用来传达信息或只是的各种形式的提示或标志; 在生活中常见的信号例如红绿灯,交通标志,短信通知等 在操作系统中,"信号"是一种用于异步通知进程发生特定事件的机制;…

WebGIS的Web服务概述

WebGIS是互联网技术应用于GIS开发的产物&#xff0c;是现代GIS技术的重要组成部分&#xff0c;其中的Web服务是现代WebGIS的核心技术和重要标志&#xff0c;它集GIS、程序组件和互联网的优点于一身&#xff0c;深刻改变了GIS开发和应用的方式&#xff0c;绕过了本地数据转换和本…

Yum包下载

1. 起因 内网有一台服务器需要升级php版本,维护的同学又不想二进制安装.服务器只有一个光盘的yum仓库 2. 解决方法 解决思路如下: 外网找一台机器配置php8.3.8的仓库外网服务器下载软件集并打包内网服务器上传并解压实现升级 2.1 下载php8.3.8仓库 配置php仓库 rootcent…

Postman导出excel文件

0 写在前面 在我们后端写接口的时候&#xff0c;前端页面还没有出来&#xff0c;我们就得先接口测试&#xff0c;在此记录下如何使用postman测试导出excel接口。 如果不会使用接口传参可以看我这篇博客如何使用Postman 1 方法一 2 方法二 3 写在末尾 虽然在代码中写入文件名…

7月21日,贪心练习

大家好呀&#xff0c;今天带来一些贪心算法的应用解题、 一&#xff0c;柠檬水找零 . - 力扣&#xff08;LeetCode&#xff09; 解析&#xff1a; 本题的贪心体现在对于20美元的处理上&#xff0c;我们总是优先把功能较少的10元作为找零&#xff0c;这样可以让5元用处更大 …

OpenAI发布迷你AI模型GPT-4o mini

本心、输入输出、结果 文章目录 OpenAI发布迷你AI模型GPT-4o mini前言OpenAI发布迷你AI模型GPT-4o mini英伟达联合发布 Mistral-NeMo AI 模型:120 亿参数、上下文窗口 12.8 万个 tokenOpenAI发布迷你AI模型GPT-4o mini 编辑 | 简简单单 Online zuozuo 地址 | https://blog.csd…

vulnhub——Ai-Web1靶机渗透

Ai-Web1靶机渗透 靶机下载&#xff1a; 官网地址&#xff1a;https://www.vulnhub.com/entry/ai-web-1,353/ 攻击机&#xff1a;kali2024 一、信息收集 发下目标主机的IP为&#xff1a;192.168.201.141 用nmap工具扫描一下对方主机和服务 发现他打开了80端口 发现搜不到于是…

提升无线网络安全:用Python脚本发现并修复WiFi安全问题

文章目录 概要环境准备技术细节3.1 实现原理3.2 创建python文件3.3 插入内容3.4 运行python脚本 加固建议4.1 选择强密码4.2 定期更换密码4.3 启用网络加密4.4 关闭WPS4.5 隐藏SSID4.6 限制连接设备 小结 概要 在本文中&#xff0c;我们将介绍并展示如何使用Python脚本来测试本…

Linux系统学习日记——vim操作手册

Vim编辑器是linux下的一个命令行编辑器&#xff0c;类似于我们windows下的记事本。 目录 打开文件 编辑 保存退出 打开文件 打开 hello.c不存在也可以打开&#xff0c;保存时vim会自动创建。 效果 Vim打开时&#xff0c;处于命令模式&#xff0c;即执行命令的模式&#x…

Web 3.0革新:社交金融与边玩边赚开启用户数据主权时代

目录 Web 3.0与社交商业模式 传统社交平台的问题 去中心化社交创新 Mirror&#xff1a;去中心化内容发布平台 Lens Protocol&#xff1a;去中心化社交图谱 Maskbook&#xff1a;隐私保护的社交方式 Web 3.0与与边玩边赚模式 经济模型解析 新商业模式的探索 Axie Infi…

【MySQL-17】存储过程-[变量篇]详解-(系统变量&用户定义变量&局部变量)

前言 大家好吖&#xff0c;欢迎来到 YY 滴MySQL系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

《0基础》学习Python——第二十二讲__网络爬虫/<5>爬取豆瓣电影封面图

一、爬取豆瓣电影的图片封面 1、经过上节课我们所爬取的豆瓣电影的电影名、年份、国家、导演、主演、剧情&#xff0c;那么接下来我们将学习如何去爬取这些电影的图片&#xff0c;并将这些图片存放在文件夹中。 2、过程实现&#xff1a; 2.1、获取网页源码 首先还是和爬取电影名…

FreeU: Free Lunch in Diffusion U-Net

FreeU&#xff1a;扩散 U-Net 模型的免费午餐 论文链接&#xff1a;https://arxiv.org/abs/2309.11497 代码链接&#xff1a;https://github.com/ChenyangSi/FreeU 项目链接&#xff1a;https://chenyangsi.top/FreeU/&#xff08;CVPR2024) Abstract 在本文中&#xff0c;…