linux线程 | 同步与互斥(上)

        前言:本节内容主要是线程的同步与互斥。 本篇文章的主要内容都在讲解互斥的相关以及周边的知识。大体的讲解思路是通过数据不一致问题引出锁。 然后谈锁的使用以及申请锁释放锁的原子性问题。 那么, 废话不多说, 现在开始我们的学习吧!

        ps:本节内容适合了解线程的相关概念的友友们进行观看哦

目录

数据不一致问题

锁的使用

理解锁的竞争

申请锁与释放锁的原子性问题


数据不一致问题

        我们之前写过g_val全局变量,也就是共享资源。 问题是,我们的线程在访问共享资源的时候, 会不会发生一个线程正在访问,但是另一个线程修改了共享资源的情况呢? 此时这个时候就可能造成因为共享导致的数据不一致问题!

        我们这里写一个代码啊, 用来模拟一下多线程抢票的过程:


#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#include<unistd.h>
#include<pthread.h>
#include<string>
using namespace std;

int tickets = 1000;
#define NUM 4

//线程的属性描述方法
class threadData
{
public:
    threadData(int number)
    {
        threadname = "thread-" + to_string(number);
    }
    string threadname;
};

//多线程的执行代码
void* GetTicket(void* args)
{
    threadData* td = static_cast<threadData*>(args);

    //抢票 
    while(true)
    {
        if (tickets > 0)
        {
            usleep(1000);
            cout << td->threadname << " get a tickets: " << tickets << endl;
            tickets--;
        }
        else break;
    }
    //抢完票后退出
    cout << "quit " << endl;
    

    return nullptr;    
}
int main()
{
    //创建两个数组, 用来组织创建的多线程的pid和线程属性
    vector<pthread_t> tids;
    vector<threadData*>thread_datas;
    
    //创建多线程
    for (int i = 1; i <= NUM; i++)
    {
        //
        pthread_t tid;
        threadData* td = new threadData(i);
        
        //创建多线程
        pthread_create(&tid, nullptr, GetTicket, td);
        tids.push_back(tid);
    }
    
    //等待多线程
    for (auto e : tids)
    {
        pthread_join(e, nullptr);
    }

    //释放多线程的属性
    for (auto td : thread_datas)
    {
        delete td;
    }

    return 0;
}

 这串代码的运行结果为:

        我们可以看到,代码出现了负数的情况。 这是为什么呢? 

        我们从上面的知识就能知道, tickets是属于所有线程并发的共享变量。而这种票被减到负数的情况叫做共享数据在无保护的情况下被多线程并发访问, 造成了数据不一致问题。

        什么是数据不一致问题呢? 首先我们知道, 数据不一致问题肯定是和多线程并发访问有关系的, 那么假如我们的一个线程在tickets减减的时候其他的线程也来了, 就有可能造成数据不一致的问题, 我们下面就来理解数据不一致问题:

        我们要理解上面的问题, 就要先谈一谈tickets--, 下面是cpu和内存:

        首先我们定义全局变量, 不管我么如何定义全局变量, 这个全局变量的本质一定是在内存当中的。 就比如上面这个tickets。 假如一开始tickets是1000, 那么, 我们对tickets做减减操作, 它的本质就是在做计算。 在我们的整个计算机里面, 我们认为, 要在计算机里面做计算, 其实本质上就是在cpu里面做计算。 可是数据在内存中, 所以我们tickets的第一步就是线程tickets的数据读入到cpu的寄存器当中。然后第二步在cpu中做减减操作。 第三步再将数据写回内存。 ——这三步每一步都对应着一条汇编操作:1、move[XXX] eax; 2、 --; 3、move eax [XXX]

        然后上面是单线程的情况, 那么多线程就是多个线程每一个线程都去做上面的三个步骤, 假如我们现在有两个线程:线程1和线程2

假如这是线程1, 一开始线程1要将内存中的数据读取到cpu当中。

        读取完成之后,我们要知道, 任何一个线程, 在执行任何一个代码之后, 都有可能被切换(因为时间片的缘故。 假如线程1刚刚执行完第一步, 正准备执行第二步的时候, 线程1就要被切走了, 那么他既然要被切走了, 那么就要把上下文也给带走。注意, 寄存器不等于寄存器的内容。 cpu的寄存器只有一套, 但是每一个线程在运行期间他都是要用cpu这一套寄存器。 但是当他走的时候要把寄存器里面保存的内容带走, 这叫保存上下文。 保存上下文的目的是为了恢复。 所以, 寄存器只有一套, 但是每个线程都有自己对应的寄存器对应的数据。 这个数据每个线程都有。

        那么, 此时线程2来了:

        线程2也要减减, 那么第一部数据加载到cpu, 第二步减减, 第三步cpu加载到内存。 问题是线程2很幸运地三步一口气执行完毕。 所以此时的tickets就变成999了。 

        而且, 假设线程2运气很好, 它重复减减, 一直到了tickets为10. 那么等到线程2再次进行--的时候, 刚刚加载到cpu, 就被切换走了。 所以此时线程2的上下文就是10. 

但是线程1回来的第一件事情就是恢复上下文, 此时他认为cpu寄存器中应该是1000, 所以他就将数据加载到cpu。 然后--到999, 最后再加载到内存!

        那么问题来了, 我们的线程2本来都已经减到10了, 但是线程1一下子又将数据变回来了。 ——这就是典型的数据不一致问题!

        我们今天的抢票, 不仅仅是在减减, 而且还在进行判断。 所以呢, 为什么我们的抢票会出现负数? 我们知道, 判断其实就是运算, 叫做逻辑运算。 那么我们一个线程在逻辑运算的时候, 其他的线程可没可能也在进行逻辑运算呢? 答案是非常有可能。 所以就有一个情况——就是我们的线程1, 2, 3, 4同时都在进行判断, 并且此时的tickets为1. 然后, 他们都判断成功, 然后第一个线程减减到零。  第二个线程减减要先读取, 上一个已经减到零了, 所以他读取, 就是0减减, 然后得到-1. 第三个读到-1, 减减到-2, 第四个读到-2, 减减到-3. 所以就出现了图中的情况。 而这, 也就是数据不一致问题。(注意, 其实总结一下tickets--, 就是tickets--具备执行中的概念, 不是原子的!所以它可以正在--的时候被打扰。)

        锁使用解决数据不一致问题的。 下面我们来看一下锁的创建以及销毁:

        我们所说的锁, 其实就是pthread_mutex_t类型的一种数据结构。 对应的, 有初始化这个数据结构pthread_mutex_init函数, 有销毁这个数据结构pthread_mutex_destroy函数。 其中, pthread_mutex_init的第一个参数是要初始化的锁对象, 第二个参数就是要设置的属性(后续我们会用init函数, 第二个参数我们本篇文章不关心)

        我们在使用pthread_mutex_t的时候, 定义它有两种方案:第一种是直接定义成为全局的, 然后利用PTHREAD_MUTEX_INITIALIZER进行初始化(也可以使用init函数)、如果是一把常见的锁, 那么就只能使用init函数了。 

现在我们来看一下具体的加锁函数:

        pthread_mutex_lock这个函数就是利用当前的锁加上锁。 这个参数是锁对象的地址。 然后又lock, 就要有unlock, 也就是第三个函数是解锁。 

         关键是我们在哪里加锁——我们回忆一个问题, 就是一个tickets全局变量, 这个全局变量在线程中可以被叫做共享变量。 那么在并发访问时, 我们不想让他因为并发的原因出现问题, 所以我们就要对tickets的访问的地方加锁, 如果我们万一成功加锁了, 我们就把这个曾经被我们共享的全局变量叫做临界资源。 在我们的所有的代码当中, 是不是所有的代码, 都在访问临界资源呢? 答案是并不是, 我们的多线程在访问加锁是不是好的事情呢? ——加锁的本质其实就是让被加锁区域串行访问。 因为任何一个时刻只允许一个线程去访问这个代码区 所以, 加锁的本质其实是利用时间换取安全。 

        而加锁的表现:线程对于临界区代码串行执行。 所以加锁的原则就是尽量的保证临界区代码越少越好!!

锁的使用

看下面的代码就是锁的一个使用例子。 这里使用的是我们上面的买票的代码。

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("who=%s, get a ticket: %d\n", name, tickets);
            tickets--;
            pthread_mutex_unlock(td->lock); 
        }
        else 
        {
            pthread_mutex_unlock(td->lock); 
            break;
        } 
        usleep(13);
    }
    return nullptr;
}

        这里的阻塞等待,如何理解? 其实就是一个线程当前没有拿到锁, 那么cpu就将这个线程的pcb放到对应的锁的调度队列当中去等待。

        上面这串代码中, 我们要知道的是, 不同的线程对于锁的竞争力度是不一样的。 一般情况下, 我们如果不加上面的这个usleep(13), 肯定会造成线程们的竞争力不一样,导致的单个线程一直抢到锁的问题。就如同下图:

        这也侧面证明了, 我们的每一个线程对于锁的竞争力度是不一样的!所以为了解决这个问题我们就把每一个线程解锁后都休息上几微妙,就能防止出现这种情况了:

理解锁的竞争

        现在我们就着上面锁的竞争性不一样的情况讲一个故事来理解几个概念。

  •         就比如有上面一间vip自习室。但是这个自习室依次只允许一个人进入。 所以呢, 每天早晨小王就来到自习室这里抢占位置。 当小王进入自习室学习的时候, 那么这个时候他把门一反锁。 这个时候这个自习室的占用权就是他的了。 但是呢, 这个时候是不是外边陆陆续续的又有人来了, 他们一看到门关着, 门外的钥匙没了, 人们就只能在外面等着。——这就是多线程的阻塞等待。 
  •         当小王学习了一会儿, 坐不住了, 想要出去溜溜。 然后小王出门将钥匙挂在墙上, 但是一看到这么多人, 下次回来不一定能够排上了, 所以就又拿着钥匙回去了。 但是呢, 一会又坐不住了, 又要出门, 但是看到这么多人又后悔了。反反复复, 因为小王是离钥匙最近的, 所以它的竞争力度是非常大的。 所以就导致了长时间拿不到钥匙的外面的人们的饥饿问题——这, 也是多线程的饥饿问题。 

        所以, 纯互斥环境, 如果锁分配不够合理, 容易导致其他线程的饥饿问题。 注意, 不是说只要有互斥必有饥饿。 而是说在互斥的条件下找到纯互斥的的场景, 就用互斥!

        那么现在有个观察员

        这个观察员看到小王光呆在自习室, 也不创造价值。 所以就设定了规则——

  • 1:外面的人, 必须排队。
  • 2:出来的人, 不能立马申请锁, 必须排队到队列的尾部!

        这样, 就能让所有的人按照一定的顺序获取锁(钥匙)。而我们上面锁的使用里面使用的usleep(13)其实就是模拟的第二点!!而这,这种按照一定的顺序性获取的资源叫做同步!!

申请锁与释放锁的原子性问题

        现在我们知道了, 我们通过加锁和解锁限制了一块临界区。 可是, 每一个线程在进入临界区访问临界资源的时候, 它的第一件事情都是申请同一把锁。 那么此处每一个线程它要执行临界区的代码,它就要先获得一把锁。 所以, 这把锁本身就应该是一个临界资源(共享资源)。 所以, 每个线程为了保护我们自己访问临界区是安全的, 但是我们在访问的时候, 谁来保证访问锁是安全的呢? ——其实, 申请锁和释放锁本身就被设计成为了原子性操作。 问题是, 如何做到的呢?

        那么上面绿色框框就是我们的临界区。 首先我们需要知道的是, 处在临界区中线程可以被切换吗? ——我门说过tickets都不是原子的, 都是可以被切换的。 那么这么一大块代码, 就不可以切换了吗? 所以, 在临界区里面, 我们的线程是可以被切换的。

        知道了这些, 那么我们就可以来看上面的问题了:我们还是使用小王的例子来说。 就比如小王想上厕所, 但是小王因为出去的话回来还要重新排队。 所以他想了一个办法, 就是出去的时候将钥匙不放回原位置, 将钥匙装在兜里, 这样回来的时候就不用排队了, 直接可以进入到屋子里。

        所以, 线程虽然可以被切换, 但是我们的线程怕不怕被切换呢 答案是不怕 因为我们的线程被切出去的时候, 可以持有锁被切出去 即便我线程没在被cpu执行, 但是只要我没有mutex_unlock, 那么其他线程就拿不到锁!!

        所以,这个临界区的代码对于线程来说,只有两种是有意义的——要么已经释放了锁, 要么正在申请锁。 也就是说,我们的其他线程知道自己没有机会的时候, 它也就不关心正在执行的线程的中间代码了, 而是去关心线程现在有没有把锁释放, 有没有在重新竞争锁。         所以, 其他的线程在关注正在执行的线程时, 他最关心这个线程是否已经释放完了锁!!! 因为他知道关心其他的一点意义都没有。 所以, 通过加锁, 我们就能保证, 我们当前线程在访问临界区期间对于其他线程来讲时原子的 所以对于其他线程来讲, 一个线程要么申请锁, 要么释放锁。 所以, 它们是原子的!!!

  ——————以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!   

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于element-ui的upload组件与阿里云oss对象存储的文件上传(采用服务端签名后直传的方式)

服务端签名后直传图解 步骤 1 开通阿里云OSS对象存储服务&#xff0c;创建新的Bucket 2 创建子账户获取密钥 创建用户 添加权限 后端 1 新建一个第三方服务的模块 third-party pom文件 <?xml version"1.0" encoding"UTF-8"?> <project x…

【工程测试技术】第4章 常用传感器分类,机械式,电阻式,电容式,电感式,光电式传感器

上理考研周导师的哔哩哔哩频道 我在频道里讲课哦 目录 4.1 常用传感器分类 4.2 机械式传感器及仪器 4.3 电阻式、电容式与电感式传感器 1.变阻器式传感器 2.电阻应变式传感器 3.固态压阻式传感器 4.典型动态电阻应变仪 4.3.2 电容式传感器 1.变换原理 2.测量电路 …

ScriptableObject基本使用

使用方法 自定义类继承ScriptableObject 可以在类内部增加数据或者数据类&#xff0c;一般用于配置 注意事项 给继承ScriptableObject的类增加CreateAssetMenu特性。 CreateAssetMenu一般默认三个参数 第一个参数是父目录 第二个参数是父目录的子选项 第三个参数是可以…

[瑞吉外卖]-05菜品模块

文件上传下载 介绍 文件上传也称为upload&#xff0c;是指将本地图片、视频、音频等文件上传到服务器上, 可以供其他用户浏览或下载 前端组件库提供了上传组件&#xff0c;但是底层原理还是基于form表单的文件上传。 服务端要接收客户端上传的文件&#xff0c;通常都会使用Ap…

QT--QPushButton设置文本和图标、使能禁能、信号演示

按钮除了可以设置显示文本之外&#xff0c;还可以设置图标 文本 可以获取和设置按钮上显示的文本 // 获取和设置按钮的文本 QString text() const void setText(const QString &text)该属性&#xff0c;既可以在 Qt 设计师右侧的属性窗口中修改&#xff0c;也可以在代码…

深度学习调参技巧总结

文章目录 深度学习调参技巧总结1.寻找合适的学习率2.优化算法选择3.模型对不同超参数的敏感性4.训练技巧参考 深度学习调参技巧总结 1.寻找合适的学习率 学习率&#xff08;Learning Rate, LR&#xff09;是机器学习模型训练中极其重要的超参数。它直接影响模型的收敛速度和最…

数据结构——排序(2)

数据结构——排序(2) 文章目录 数据结构——排序(2)前言&#xff1a;1.快速排序&#xff08;非递归版本&#xff09;基本步骤&#xff1a;代码实现 2.归并排序算法思想&#xff1a;核心步骤&#xff1a;代码实现&#xff1a;特征总结&#xff1a; 3.计数排序&#xff08;非比较…

【深度学习系统】Lecture 2 - ML Refresher / Softmax Regression

一、问题的理解方式 首先&#xff0c;什么是数据驱动的编程&#xff1f;面对经典的MNIST数据集识别任务&#xff0c;传统的编程思维和数据驱动的编程思维有何不同&#xff1f; 传统编程思维&#xff1a; 通常从明确的问题定义和具体的算法开始。对于 MNIST 数据集识别任务&a…

AI时代的神器,解锁 PPT 制作新体验--分享使用经验

背景&#xff1a;探讨人们在使用AI工具时&#xff0c;最喜欢的和认为最好用的工具是哪些&#xff0c;展示AI技术的实际应用和影响。 说明&#xff1a;本文分析的AI技术的实际应用是制作PPT的AI工具。>>快速访问本文的AI工具<< 你好&#xff0c;我是三桥君 你有没有…

网络抓包06 - Socket抓包

TCP thread {val socket Socket("xx.xxx.xxx.xx", 8888)socket.soTimeout 3000val os socket.getOutputStream()Log.e("Socket", "class name ${os::class.java.canonicalName}")os.write(0x00)}运行代码&#xff0c;得知 OutputStream 是 S…

Python 工具库每日推荐 【sqlparse】

文章目录 引言SQL解析工具的重要性今日推荐:sqlparse工具库主要功能:使用场景:安装与配置快速上手示例代码代码解释实际应用案例案例:SQL查询分析器案例分析高级特性自定义格式化处理多个语句扩展阅读与资源优缺点分析优点:缺点:总结【 已更新完 Python工具库每日推荐 专…

文件的读写、FileStream

//现在在desktop\10.13文件夹下的读写文件,由上知空空如也。 if (File.Exists(@"C:\Users\11442\Desktop\10.13\FILE.txt")) { File.Delete(@"C:\Users\11442\Desktop\10.13\FILE.txt"); File.Create(@"C:\Users\11442\Desktop\10.13\FIL…

(IOS)VMware虚拟机上安装win10系统(超详细)

简介 虚拟机是一种软件实现的计算机系统&#xff0c;可以在现有的操作系统平台上运行一个或多个虚拟的操作系统。它通过在主机操作系统上创建一个虚拟的硬件平台&#xff0c;并在其上运行一个完整的操作系统&#xff0c;来模拟一个真实的物理计算机。虚拟机可以提供一种隔离的…

多线程代码案例

案例一.单例模式 单例模式是一种设计模式;类似于棋谱,有固定套路,针对一些特定场景可以给出一些比较好的解决方案; 只要按照设计模式来写代码,就可以保证代码不会太差,保证了代码的下限; --------------------------------------------------------------------------------…

接口测试面试题含答案

1、解释一下正向和逆向测试。 正向测试&#xff1a;针对接口设计预期的功能和行为&#xff0c;验证接口是否按照预期工作。 逆向测试&#xff1a;针对错误输入、不合理的条件或非预期的使用方式&#xff0c;验证接口是否能够适当地处理这些情况并提供合理的错误处理。 2、什…

Windows11下 安装 Docker部分疑难杂症(Unexpecter WSL error)

装了大半天Docker desktop终于装好了&#xff0c;网上有的主流教程就不复述了&#xff0c;主要说一下网上没有的教程。 以下是遇到的问题&#xff1a; 首先&#xff0c;启用或关闭Windows确保里面与虚拟机有关的几个都要选上 没有Hyper-V参考此文 但是我这里都勾选了&#x…

Unity/VS 消除不想要的黄色警告

方法一&#xff1a;单个消除 在要关闭的代码前一行写上#pragma warning disable 警告代码编码 在要关闭代码行下面一行写上#pragma warning restore 警告代码编码 精准的关闭指定地方引起的代码警告&#xff0c;不会过滤掉无辜的代码 #pragma warning disable 0162,1634HandleL…

react实现实时计时的最简方式

js中时间的处理&#xff0c;不借助于moment/dayjs这样的工具库&#xff0c;原生获取格式化的时间&#xff0c;最简单的实现方式可以参考下面这样。 实现效果 代码实现 封装hooks import { useState, useEffect } from "react";export function useCountTime() {c…

C语言笔记 14

函数原型 函数的先后关系 我们把自己定义的函数isPrime()写在main函数上面 是因为C的编译器自上而下顺序分析你的代码&#xff0c;在看到isPrime的时候&#xff0c;它需要知道isPrime()的样子——也就是isPrime()要几个参数&#xff0c;每个参数的类型如何&#xff0c;返回什么…

图解C#高级教程(五):枚举器和迭代器

本章主要介绍 C# 当中枚举器、可枚举类型以及迭代器相关的知识。 文章目录 1. 枚举器和可枚举类型2. IEnumerator 和 IEnumerable 接口2.1 IEnumerator 接口2.2 IEnumerable 接口 3. 泛型枚举接口4. 迭代器4.1 使用迭代器创建枚举器4.2 使用迭代器创建可枚举类4.3 迭代器作为属…