【Linux】多线程(互斥 同步)

news2024/11/20 14:20:12

我们在上一节多线程提到没有任何保护措施的抢票是会造成数据不一致的问题的。

那我们怎么办?
答案就是进行加锁。

目录

  • 加锁:
    • 认识锁和接口:
      • 初始化:
      • 加锁 && 解锁:
        • 全局的方式:
        • 局部的方式:
    • 原理角度理解:
    • 实现角度理解:
  • 同步:

加锁:

认识锁和接口:

初始化:

在这里插入图片描述
这个就是我们互斥锁的类型。其中互斥代表任何时刻只允许一个线程进行访问,锁代表为了实现互斥提供的一种方式。

锁不是一个单纯的内置类型,而是一个在这里插入图片描述
那么就肯定要对他进行初始化。
其中我们有两种初始化方式。
对于全局的锁,我们使用宏的方式。
在这里插入图片描述
而局部的锁我们则需要进行init初始化。

全局的所使用宏初始化后就不需要进行destory,因为随着生命周期的结束,会自动被系统回收。

但是局部的锁使用结束时要使用destory进行销毁。

加锁 && 解锁:

在这里插入图片描述
对于锁我们现在要有一个理解:对于多线程时,每个线程都会竞争这把锁,但只有一人会竞争成功,失败的会被阻塞,直到锁被解锁。

我们的锁是进行保护临界区的,或者说是保护临界资源。

而我们保护临界资源,本质是对临界区代码进行保护,怎么样理解这句话呢?

我们所有的资源都是通过代码进行访问的,所以本质上就是把访问资源的代码保护起来。

在这里插入图片描述
加锁之后当然要进行解锁在这里插入图片描述
所以我们就来改进一下上一章节产出的封装线程库 + 抢票的代码。

全局的方式:

我们要先看一下错误的加锁方式:
在这里插入图片描述
现象:
在这里插入图片描述
原因:因为只有一个线程会抢到锁,而对于上图程序而言一旦加锁就势必要把全部的票数抢完才可以解锁,也就意味着别的线程都无法抢票了。

所以上图的加锁方式是错误的,失去了多线程的意义。

正确的加锁:

在这里插入图片描述
现象:
在这里插入图片描述
在这里插入图片描述

结论:

  1. 加锁的范围要小粒度,非临界区是并行执行,临界区是串行执行,当你的粒度过大,串行的就多了,效率就低下了。
  2. 任何线程,进行抢票都需要申请锁,并不能因为程序是你写的而是个别线程出现特例。
  3. 所有的线程都申请锁,前提是所有的线程都可以看到这把锁,这意味着锁是共享资源,如何保证锁的安全?锁是原子的!
  4. 原子性:要么没做,要么就是做完,没有中间状态。他的反例就是吃饭,吃饭有没吃,也有吃了,但是还有吃饭中.
  5. 线程申请锁失败了就要被阻塞
  6. 线程申请锁成功继续运行
  7. 如果线程申请锁成功了,在执行临界区代码,在执行临界区代码期间可以被切换吗?
    答案是可以的,并且其他线程依旧无法进入,因为被切换的进程带着锁走了并没有释放!

结论:对于没有锁的线程,只有申请了锁的线程释放了线程才是有意义的。

其实对于访问临界区,对于无锁线程来说也是原子的。

局部的方式:

局部锁我们就要修改一下上节课的代码:
首先创建mythread时就不能单纯的只有name,还要再多一个mutex指针,于是我们选择传一个结构体指针即可。
在这里插入图片描述
在这里插入图片描述
具体变动如下,随着传入数据的修改也要修改一下连带的部分,造成了牵一发动全身的情形。
而引入了模板就可以避免这些问题,这就是模板的好处,但是由于这里我们并没有使用模板,所以暂时只能这样

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <pthread.h>

class ThreadData
{
public:
    ThreadData(const std::string name, pthread_mutex_t* mutex) : _name(name), _mutex(mutex)
    {
    }

public:
    std::string _name;
    pthread_mutex_t* _mutex;
};
namespace cyc
{

    class mythread
    {
    public:
        typedef void (*func_t)(ThreadData*);
        mythread(ThreadData* td, func_t func) : _td(td), _func(func), _isRunning(false)
        {
        }
        ~mythread()
        {
        }

        void Excute()
        {
            _isRunning = true;
            _func(_td);
            _isRunning = false;
        }
        static void *routine(void *arg)
        {
            mythread *self = static_cast<mythread *>(arg);
            self->Excute();
            return nullptr;
        }

        void Start()
        {
            int n = ::pthread_create(&_tid, nullptr, routine, (void *)this);
            if (n != 0)
            {
                perror("pthread_create fail");
                exit(1);
            }
        }
        void Stop()
        {
            if (_isRunning)
            {
                pthread_cancel(_tid);
                _isRunning = false;
            }
        }
        void Join()
        {
            int n = pthread_join(_tid, nullptr);
            if (n != 0)
            {
                perror("pthread_join fail");
                exit(1);
            }
            std::cout << _td->_name << " Join sucess..." << std::endl;
            delete _td;
        }
        std::string GetStatus()
        {
            if (_isRunning)
                return "Running";
            else
                return "sleeping";
        }

    private:
        ThreadData *_td;
        pthread_t _tid;
        func_t _func;
        bool _isRunning;
    };
}

传参时new一下
在这里插入图片描述
加锁时直接找td的成员即可。
在这里插入图片描述
此外我们还有另一种加锁方式,称之为RAII风格。
在这里插入图片描述
定义一个局部变量,当此时循环开始或者结束时自动创建,而正好这个类的构造与析构包含了lock与unlock,避免了繁琐的上下锁。
在这里插入图片描述

原理角度理解:

其实我们在加锁与解锁已经说明了很大一部分原理了。
在这里插入图片描述

接下来我们要探讨一下为什么lock后,
申请到锁的线程会继续执行程序,而其他线程会阻塞住?
最主要的原因就是申请到锁的线程在lock中会返回一个值,从而继续运行,而申请失败的线程则会不返回,线程就是阻塞了。

另外,我们的lock函数与pthread_mutex_t这个类型都是Pthread库中的,这个库会维护这套东西,当申请失败就会线程状态设置为S,放入等待队列中,当申请成功的线程unlock后,阻塞在lock函数内部的线程被重新唤醒,继续申请锁,重复以上步骤。

实现角度理解:

到实现层面上我们就必须谈谈原子性。

原子性在概念上是两态的,一条汇编就是原子的,他有多种体现形式,比如我们说过的抢票代码。

这里插个嘴,比较深的了解一下硬件,对整体节奏无影响:

我们的程序经过编译之后形成汇编,就像printf->十几行汇编->二进制:此时我们的二进制由两部分构成,一部分是二进制数据(int float…),一部分是二进制指令(被CPU进行执行)。
可是CPU怎么认识二进制指令?
CPU除了有寄存器构成,还有硬件电路构成,其中我们的数据寄存器中存储着数据,当我们进行加减乘除时怎么操作?CPU具有指令集,指令集可以知道执行什么操作,但具体怎么执行要靠硬件电路。
所以CPU在设计时就存在指令集这一概念。
因为CPU最初可以认识加减乘除,所以我们最开始的程序是由二进制编写的,比如纸带…–>但是效率太低,我们就有了汇编,也就有了编译器-------->C/C++。
这些语言都在指令集的基础上才得以存在。

为了实现互斥锁操作,大多数体系结构(CPU)都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码看一下
在这里插入图片描述
现在我们将这个伪代码走一遍可以更好的感受锁(下图是一些辅助知识帮助理解)。

在这里插入图片描述

在这里插入图片描述
具体步骤:
在这里插入图片描述

同步:

我们可以观察到抢票的结果:其中一个线程抢票很频繁,这也是我们同步要解决的问题。

我们现在就要举例子进行一个形象的解释:

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

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

相关文章

rtpengine_mr12.0 基础建设容器运行

目录 Dockerfile rtpengine.conf 容器内编译安装 RTPEngine 正常提供功能 1. 启动RTPEngine服务 2. 删除 RTPEngine服务 3. 加载内核模块 检查所有进程是否正在运行 上传到仓库 博主wx&#xff1a;yuanlai45_csdn 博主qq&#xff1a;2777137742 后期会创建粉丝群&…

地埋RF射频电子标识器探测仪ED8000(V400版)使用操作说明之1测量准备工作

地埋RF射频电子标识器探测仪ED8000&#xff08;V400版&#xff09;是一台集成了多频率、多种ID标识器调制模式、高低灵敏度调节、可读写标识器等全功能、高性能电子标识器探测仪。它有着极高的灵敏度,同时具备良好的噪声抑制能力&#xff0c;不仅适合专业测绘人员&#xff0c;普…

监控平台—Zabbix对接grafana

目录 一、安装grafana并启动 二.浏览器访问 三、导入zabbix数据&#xff0c;对接grafana 四.如何导入模版 一、安装grafana并启动 添加一台服务器192.168.80.102 初始化操作 systemctl disable --now firewalld setenforce 0 vim /etc/selinux/config SELINUXdisabled cd /…

东哥教你如何用Orange Ai pro为家里做一个垃圾分类检测机器

前言 最近入手了一块香橙派&#xff08;Orange Ai Pro&#xff09;的板子&#xff0c;他们的口号是&#xff1a;为AI而生&#xff0c;这让一个算法工程师按捺不住了&#xff0c; 之前主要是在RKNN和ESP32等设备上部署AI模型&#xff0c;看到官方介绍的强大AI算力&#xff0c;很…

入门PHP就来我这(纯干货)08

~~~~ 有胆量你就来跟着路老师卷起来&#xff01; -- 纯干货&#xff0c;技术知识分享 ~~~~ 路老师给大家分享PHP语言的知识了&#xff0c;旨在想让大家入门PHP&#xff0c;并深入了解PHP语言。 1 PHP对象的高级应用 1.1 final关键字 final 最终的、最后的。被final修饰过的类…

如何在电脑设备上恢复已删除的照片

丢失 PC、智能手机或 USB 闪存盘上的照片可能会让人不知所措。幸运的是&#xff0c;使用最好的照片恢复软件&#xff0c;您可以在 Windows 和 Android 上恢复已删除的照片。本博客讨论如何使用照片恢复来恢复丢失的照片。 数码照片是我们记忆的重要组成部分。然而&#xff0c;它…

UE4_材质基础_切线空间与法线贴图

学习笔记&#xff0c;不喜勿喷&#xff0c;侵权立删&#xff0c;祝愿大家生活越来越好&#xff01; 一、切线空间 在《OpenGL基础11&#xff1a;空间》中提到了观察空间、裁剪空间、世界空间等。切线空间和它们一样&#xff0c;都属于坐标空间 上面就是一个…

FPGA的理解,个人的见解,不一定对

类似于面包板上搭建电路&#xff0c;但是使用的是逻辑单元模块&#xff1b;如加法器&#xff0c;减法器&#xff0c;寄存器等 没有模拟电路的电容&#xff0c;电阻&#xff1b;但是逻辑单元的底层实现&#xff0c;使用MOS管等电路实现电路的开关&#xff1b;从而表示0&#xf…

springai+pgvector+ollama实现rag

首先在ollama中安装mofanke/dmeta-embedding-zh:latest。执行ollama run mofanke/dmeta-embedding-zh 。实现将文本转化为向量数据 接着安装pgvector&#xff08;建议使用pgadmin4作为可视化工具&#xff0c;用navicate会出现表不显示的问题&#xff09; 安装好需要的软件后我们…

【python】OpenCV—Nighttime Low Illumination Image Enhancement

文章目录 1 背景介绍2 代码实现3 原理分析4 效果展示5 附录np.ndindexnumpy.ravelnumpy.argsortcv2.detailEnhancecv2.edgePreservingFilter 1 背景介绍 学习参考来自&#xff1a;OpenCV基础&#xff08;24&#xff09;改善夜间图像的照明 源码&#xff1a; 链接&#xff1a…

vue2 webpack使用optimization.splitChunks分包,实现按需引入,进行首屏加载优化

optimization.splitChunks的具体功能和配置信息可以去网上自行查阅。 这边简单讲一下他的使用场景、作用、如何使用&#xff1a; 1、没用使用splitChunks进行分包之前&#xff0c;所有模块都揉在一个文件里&#xff0c;那么当这个文件足够大、网速又一般的时候&#xff0c;首…

原厂商是什么意思?云管平台原厂商有哪些企业?

最近不少IT小伙伴在问关于原厂商相关问题&#xff0c;今天我们就来简单回答一下&#xff0c;仅供参考&#xff01; 原厂商是什么意思&#xff1f; 原厂商&#xff0c;或称原厂&#xff0c;是指生产特定产品或零部件的原始厂家。 软件原厂商是什么意思&#xff1f; 软件原厂…

课设:选课管理系统(Java+MySQL)

在本博客中&#xff0c;我将介绍用Java、MySQL、JDBC和Swing GUI开发一个简单的选课管理系统。 技术栈 Java&#xff1a;用于编写应用程序逻辑MySQL&#xff1a;用于存储和管理数据JDBC&#xff1a;用于连接Java应用程序和MySQL数据库Swing GUI&#xff1a;用于构建桌面应用程…

Let‘s Encrypt免费SSL证书申请最简单的步骤

随着互联网的飞速发展&#xff0c;网络安全问题愈发凸显其重要性。而HTTPS协议作为保障网站数据传输安全的重要手段&#xff0c;已经得到了广泛的应用。 申请Lets Encrypt免费泛域名SSL证书步骤 登录来此加密网站&#xff0c;输入域名&#xff0c;可以勾选泛域名和包含根域。…

Appium环境搭建,华为nova8鸿蒙系统(包括环境安装,环境配置)(一)

1.安装代码工具包 appium python client pip install appium-python-client 2.安装JDK 参考链接: ant+jmeter+jenkins从0实现持续集成(Windows)-CSDN博客 3.下载并安卓SDK 下载地址:AndroidDevTools - Android开发工具 Android SDK下载 Android Studio下载 Gradle下载…

搜维尔科技:详谈ART的工具追踪技术

您的生产流程中是否已经受益于刀具跟踪系统&#xff1f;您是否意识到它们的价值&#xff1f;因为它们可以优化您的装配顺序&#xff0c;从而节省您的时间和金钱。 目前我们提供两种工具跟踪解决方案&#xff1a; 1.ART与 VERPOSE的解决方案——易于使用的图像识别 安装在工…

C语言 | Leetcode C语言题解之第213题打家劫舍II

题目&#xff1a; 题解&#xff1a; int robRange(int* nums, int start, int end) {int first nums[start], second fmax(nums[start], nums[start 1]);for (int i start 2; i < end; i) {int temp second;second fmax(first nums[i], second);first temp;}retur…

[激光原理与应用-97]:南京科耐激光-激光焊接-焊中检测-智能制程监测系统IPM介绍 - 1 - 什么是焊接以及传统的焊接方法

目录 一、什么是焊接 1.1 概述 1.2 基本原理 二、传统的焊接技术与方法 2.1 手工电弧焊&#xff1a; 1、定义与原理 2、特点 3、焊条类型 4、应用领域 5、安全注意事项 2.2 气体保护焊&#xff1a; 1、原理与特点 2、应用领域 3、气体选择 4、注意事项 2.3 电阻…

六角法兰面螺栓机械性能

六角法兰面螺栓&#xff0c;作为一种常见的紧固件&#xff0c;因其独特的设计和优良的机械性能&#xff0c;在众多工业领域中占据重要地位。与传统的六角头螺栓相比&#xff0c;六角法兰面螺栓的底部有一个扁平的法兰面&#xff0c;能够提供更大的接触面积&#xff0c;分散压力…

[leetcode] n个骰子的点数

. - 力扣&#xff08;LeetCode&#xff09; class Solution { public:vector<double> statisticsProbability(int num) {vector<double> dp(6, 1.0 / 6.0);for (int i 2; i < num; i) {vector<double> tmp(5 * i 1, 0);for (int j 0; j < dp.size()…