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

        前言:本篇文章主要讲述linux线程的互斥的知识。 讲解流程为先讲解锁的工作原理, 再自己封装一下锁并且使用一下。 做完这些就要输出一堆理论性的东西, 但博主会总结两条结论!!最后就是讲一下死锁。 那么, 废话不多说, 我们开始学习吧!

        ps:本节内容建议先了解一下数据不一致问题以及锁的使用的友友们进行观看哦。

目录

锁的原理

锁的封装

接口

测试

可重入VS线程安全

概念

结论

死锁的概念

要如何解决死锁问题呢? 


        我们回忆一下上一节, 上一节我们知道了:为了提高并发度, 所以有了多线程。 为了使用多线程, 我们就有了线程之间的资源共享。 而资源共享又引入了多线程的访问数据不一致的问题。 因为数据不一致问题, 我们又引入了互斥锁, 而互斥锁需要考虑临界资源和临界区以及原子性和互斥性的概念。——这些就是上一节的大体内容。 现在看本节的内容吧!

锁的原理

        我们现在来重新思考一下一个问题——tickets--为什么不是原子的? 因为汇编会变成三条汇编语句。 我们认为一条汇编语句已经是计算机中最基本的指令了。什么是原子的? 这里可以下一个定义: 我们认为, 一条汇编, 就是原子的。

        其实, cpu这个硬件资源是很笨的, 我们让他去move, sub, add。 他就按照我们的指令去做。 这里面就有一个问题, 就是cpu很笨, 我们让他干什么, 他就干什么。 但是cpu为什么知道我们让他去干什么呢? 是因为我们对应的芯片当年在制作的时候, 他得在自己的芯片的硬件电路离, 一定要以硬件的方式设计出一系列能够让硬件识别的基本指令。 这个基本指令叫做芯片的指令集(区别于代码, 就是我们的汇编move, sub, add这些)

        为了实现互斥操作, 大多数体系结构提供了swap和exchange指令。 这些指令比如swap,那么他就是一条利用汇编把寄存器里面的值和内存里面的值做交换。 由于只有一条指令, 所以就保证了交换的动作是原子的。 即便是多处理器平台(多个cpu), 我们需要知道的是, 即便我们的cpu有很多块, 但是我们的cpu和内存之间的总线只有一套。所以这么多cpu通过总线访问内存的时候就会通过总线里面的硬件——仲裁器, 来决定内存由哪一个cpu来访问。 也就是说所有的cpu在访问内存的时候还是串行的, 只是在计算的时候大家可以在双cpu或者多cpu下进行计算。

        现在看下面一串伪代码, 这串伪代码是mutex_lock的伪汇编:

move $0, %al
xchgb %al, mutex
if (al寄存器的内容 > 0){
        return 0;
} else 
        挂起等待;
goto lock;

这串伪代码怎么理解, 下面博主带友友们理解一下: 

  •         图中是一块cpu和一块物理内存。 物理内存中有一块锁, 锁里面的数据就代表锁!我们设置为1.
  •         然后第一条语句是move 0 al。意思就是将0赋值给al变量(al此时已经被加载到寄存器)。
  •         第二条语句是xchgb al mutex, 意思就是将al 和 mutex中的数据调换。这也是传统意义上申请锁的动作。然后调换后就是下图:                                                                                                    
  •         第三条语句就是对al寄存器里面的数据进行判断。 如果寄存器数据大于0。 那么就申请成功, 返回零。 如果小于零, 就申请失败, 挂起等待。

        但是!这是只有一个线程的情况, 我们可以一步一步地向后执行指令。 当有多个线程的时候就要考虑线程切换的问题了。 下面是多线程的情况(注意:寄存器 != 寄存器的内容):

  •         假设一开始线程1执行第一条语句, 将al赋值为了零。 然后线程1就要被切换走了。
  •         那么此时线程1就要将寄存器中的al数据记录下来,同时将下一步执行哪一步记录下来。这叫带走上下文! 
  •         然后呢线程2来了, 线程2开始将数据加载到寄存器, 然后al 置为0.但是线程2运气好, 他没有被切换走, 然后他就继续执行xchgb操作。 成功的将al的数据0和mutex的数据1完成了交换! 此时现成2终于要被切换走了!那么现成2就要将自己的上下文带走!!!
  •         线程2切换走了之后,线程1回来执行xchgb, 但是此时mutex的值已经是0了,线程1交换al 和 mutex相当于什么都没有交换到! 然后执行判断, 对不起, 判断失败, 线程1需要挂起等待。
  •         那么线程2回来之后继续执行判断, 注意, 此时原本的mutex的那个1就在线程2的上下文中!那么线程2判断结果一定会正确, 那么线程2就申请成功了锁! 

        所以, 综上我们其实就能感觉出来, 上面的mutex_lock函数那哪一步最重要? 是不是就是xchgb这一步最重要。 谁先交换成功, 谁就相当于拿到了锁!!

锁的封装

接口


接下来我们对锁进行一下封装。 让锁的接口不再暴露在外面, 用户直接使用我们的接口:

 首先我们创建一个类,这个类对锁进行了一下封装:


class Mutex //创建锁的类
{
public:


private:
    pthread_mutex_t* _lock;
};

然后定义这个类的构造函数, 析构函数;以及加锁解锁函数:


class Mutex
{
public:
    //构造函数
    Mutex(pthread_mutex_t* lock)
        :_lock(lock)
    {} 
    //加锁
    void Lock()
    {
        pthread_mutex_lock(_lock);
    }
    //解锁
    void Unlock()
    {
        pthread_mutex_unlock(_lock);
    }
    //析构函数
    ~Mutex()
    {}

private:
    pthread_mutex_t* _lock;
};

        然后我们创建一个类似于“开关”的对象。 这个对象只要创建, 就代表我们的加锁;这个对象一小会就代表我们的解锁!如下这里面封装了我们上面刚刚定义的Mutex类的对象。


class LockGuard
{
public:

private:
    Mutex _mutex;
};  

        那么如何实现这种“资源创建即初始化”的效果呢?我们可以在它的构造函数初始化_mutex成员变量并且使用_mutex的方法lock。然后析构函数就是调用_mutex的unlock

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* lock)
        :_mutex(lock)
    {
        _mutex.Lock();
    }
    //
    ~LockGuard()
    {
        _mutex.Unlock();
    }
private:
    Mutex _mutex;
};  

测试

有了上面的代码, 我们可以拿出我们之前写的买票的代码(不知道的友友请看之前一篇文章:linux线程 | 把握线程的知识要点 | 同步与互斥-CSDN博客, 也可以看下面代码):


#include<iostream>
using namespace std;
#include<pthread.h>
#include<vector>
#include<unistd.h>
#include<string.h>
#include"LockGuard.h"

#define NUM 5  //创建多个执行流, NUM为执行流个数

using namespace std;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

int* p = nullptr;
//线程的数据信息。 
struct threadData
{
public:
    threadData(int number, pthread_mutex_t* mutex)
    {
        threadname = "thread-" + to_string(number);
        lock = mutex;
    }

public:
    string threadname;
    pthread_mutex_t* lock;
};



int tickets = 1000;
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;
}

int main()
{   
    pthread_mutex_init(&lock, nullptr);

    vector<pthread_t> tids;
    vector<threadData*> thread_datas;
    //我们创建多个执行流, 为了能够验证每个线程都有一个独立的栈结构

    for (int i = 0; i < NUM; i++)
    {
        pthread_t tid;
        threadData* td = new threadData(i, &lock);
        thread_datas.push_back(td); 
        pthread_create(&tid, nullptr, getTicket, td);
        tids.push_back(tid);
    }

    for (auto e : tids)
    {
        pthread_join(e, nullptr);
    }
    

    // pthread_mutex_destroy(&lock);
    return 0;
}


        然后我们如何改动上面的代码呢? 就是换一下GetTickets函数里面的锁的创建方法, 改成直接一个LockGurard类型的对象:

void* getTicket(void* args)
{
    threadData* td = static_cast<threadData*>(args);
    const char* name = td->threadname.c_str();

    while (true)
    {
        { //这个花括号是为了规定LockGuard对象的作用域
            LockGuard lockguard(&lock); //定义临时的lockguard对象。 RAII风格的锁
            if (tickets > 0)
            {
                usleep(1000);
                printf("who=%s, get a ticket: %d\n", name, tickets);
                tickets--;
            }
            else 
            {
                break;
            } 
        }
        usleep(13);
    }
    return nullptr;
}

然后运行:

        可以看到, 和原本创建锁的结果一样。

        

可重入VS线程安全

概念

        如果多个线程同时访问一段代码的时候, 不管如何运作, 那么我们的线程都不会出现问题, 那么就称为线程安全如果我们的代码访问某些全局变量, 然后导致其他线程出现问题了, 这个就叫做线程不安全。

        重入就是同一个函数多个执行流调用, 正在被一个执行流调用的时候其他的执行流又进来了。 然后在这种情况下仍然没有出现问题的函数称为可重入函数否则称为不可重入函数。     

        线程安全和重入这两个概念是一样的概念吗?不是的, 因为重入不可重入描述的是函数的特点, 而线程安全描述的是多线程并发的特点。目前, 我们遇到的大部分函数都是不可重入的。一个函数可重入或者不可重入, 其实并没有褒贬之分,他只是在描述这个函数的特征。但是!对于不可重入的函数我们的多执行流要用, 就必须要加锁了!!!

        因为这里有一个结论性的话:一般情况下, 一个函数是不可能重入的,那么在多线程执行下, 就有可能出现问题。 但是, 如果一个函数是可重入的, 那么在多线程的调用下, 它一定也是线程安全的。  

结论

常见的线程不安全的情况:

  •         不保护共享变量的函数。
  •         函数状态随着被调用, 状态发生变化的函数。
  •         返回指向静态变量指针的函数。
  •         调用线程不安全函数的函数。

 常见的线程安全的情况:

  •         每个线程对于全局变量或者静态变量只读!
  •         类或者接口原子的!
  •         多个线程之间的切换不会导致接口结果产生二义性!

常见不可重入的情况:

  •         IO
  •         malloc/free函数等等函数
  •         可重入函数体内使用了静态数据结构 

可重入与线程安全的联系(重要)

  •         函数如果是可重入的, 那么多线程调用这个函数的过程, 就是线程安全的!
  •         函数是不可重入的, 那么一般不可多线程使用, 有可能引发线程安全问题。
  •         如果一个函数中使用了全局变量。那么这个函数既不是线程安全的, 也不是可重入的。

可重入与线程安全的区别:

  •         可重入是线程安全函数的一种
  •         可重入一定是线程安全的,但是线程安全, 不一定是可重入的。

上面最最重要的就是两个结论:

  •         线程安全是描述线程并发的问题, 可重入是描述函数特点的问题。
  •         不可重入函数在多线程访问时可能会出现线程安全问题, 但是一个函数如果可重入, 他不会有线程安全问题。

死锁的概念

        首先什么是死锁呢? ——一般在多线程访问的时候, 我们一方面持有自己的锁, 还申请其他人的锁。我们双方既不释放自己的锁, 而且要求释放对方的锁, 并且还不强占对方的锁, 只是申请对方的锁。 这就会导致死锁的问题。 以现象定义就是在多线程的情况下, 因为锁的使用导致多线程的代码都不往后执行了。

        那么问题是, 一把锁可不可能产生死锁呢?——是可以的。当我们的同一把锁被申请两次的时候,就会产生死锁,就如同下面的代码:

运行的时候就会阻塞住了:

        我们称一个线程持有一把锁, 另一个线程持有另一把锁。 他们两个却又申请对方的锁进而导致进入一种永久等待状态的情况我们称之为死锁。 就比如张三和李四买棒棒糖, 但是张三和李四兜里只有五毛钱, 棒棒糖要一块钱, 所以张三就让李四把五毛钱给他, 他给两个人买一根棒棒糖。 那李四不愿意, 李四就和张三说把五毛钱给他, 他给两个人买棒棒糖。 这两个小朋友一不释放自己手中的五毛钱, 而还一直要对方的五毛钱。这种情况叫做张三和李四陷入了死锁问题!

        那么产生死锁的必要条件有什么呢? ——》只要产生死锁, 这四个条件一定产生!!!

  •         1:互斥条件——》一个资源一次只能被一个执行流使用。——前提
  •         2:请求与保持条件——》所谓死锁就是在互相保持自己的资源, 同时还在申请着对方的资源。——原则
  •         3:不剥夺条件——就类似于张三想要对方的五毛钱,但是他不去抢。 一个执行流以获得的资源在未使用完之前,不能强行掠夺。——原则
  •         4:循环等待条件——》若干执行流之间形成的一种头尾相连的循环等待资源的关系。 就类似于张三等待李四, 李四等待张三。——重要条件

要如何解决死锁问题呢? 

        我们知道, 对于死锁来说有四个必要的条件。 也就是说死锁必须有着这四个条件。 那么我们如果想要破坏死锁, 那么是不是只需要破坏掉其中某一个条件就可以了? 

所以方法就是:

  •         破坏请求与保持——》我们可以使用trylock, 它是lock的非阻塞版本, 很容易就能破坏请求与保持原则。
  •         破坏不剥夺——》只需要将对方的锁释放掉, 就能破坏掉不剥夺条件。
  •         破坏循环与等待条件——》破坏掉这个后, 申请锁的时候按照顺序申请锁!一般加锁的顺序保持一致!

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

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

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

相关文章

Java-多线程2

什么是线程&#xff1f; 线程是 cpu调度和执行的单位。 多个线程共享进程的堆和方法区资源&#xff0c;但每个线程有自己的程序计数器、虚拟机栈和本地方法栈。 如何实现线程 继承Thread类 实现步骤&#xff1a; 创建自定义类&#xff0c;继承Thread类 重写run方法 创建自定…

深度学习面试笔试之循环神经网络(RNN)、门控循环单元(GRU)、长短期记忆(LSTM)

深度学习面试笔试之循环神经网络RNN、门控循环单元GRU、长短期记忆LSTM 循环神经网络(RNN)1. 什么是RNN1.1 RNN的应用1.2 为什么有了CNN&#xff0c;还要RNN?1.3 RNN的网络结构1.4 双向RNN1.5 BPTT算法 2. 其它类型的RNN3. CNN与RNN的区别4. 为什么RNN 训练的时候Loss波动很大…

aws(学习笔记第七课) 私有子网使用NAT服务器

aws(学习笔记第七课) AWS的私有子网使用NAT服务器 学习内容&#xff1a; AWS的私有子网使用NAT服务器 1. AWS的私有子网使用NAT服务器 在上面的例子的网络构成图中&#xff0c;可能会发现一个问题。就是Private Subnet的Apache server无法访问互联网。比如&#xff0c;当需要…

MySQL【知识改变命运】10

联合查询 0.前言1.联合查询在MySQL里面的原理2.练习一个完整的联合查询2.1.构造练习案例数据2.2 案例&#xff1a;⼀个完整的联合查询的过程2.2.1. 确定参与查询的表&#xff0c;学⽣表和班级表2.2.2. 确定连接条件&#xff0c;student表中的class_id与class表中id列的值相等2.…

Win11右键默认显示更多选项

Win11默认显示 想要效果 解决方案1 先按住Shift键&#xff0c;再按右键试试。 解决方案2 1.启动命令行&#xff0c;输入命令 reg.exe add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}\InprocServer32" /f /ve2.显示操作成功完成&#…

2024java高频面试之JVM-第二弹

什么是 STW Java 中「「Stop-The-World机制简称 STW」」 &#xff0c;是在执行垃圾收集算法时&#xff0c;Java 应用程序的其他所有线程都被挂起&#xff08;除了垃圾收集帮助器之外&#xff09;。「Java 中一种全局暂停现象&#xff0c;全局停顿」&#xff0c;所有 Java 代码…

子比美化 – WP添加网站翻译功能 | 实现国际化多语言[js翻译]

前言 本教程适用于子比主题&#xff0c;其它程序或主题请自行适配&#xff01;&#xff01;&#xff01; 图片展示 目前支持五种语言 教程开始 首先在后台自定义CSS代码中添加以下代码 .ignore:hover{color:var(--theme-color);transition:color .2s,transform .3s;}#tran…

怎么通过docker搭建一个mqtt服务器

由于debug需要排查mqtt的连接问题&#xff0c;为了方便&#xff0c;自己在云服务器上搭建一个mqtt服务器。 文中涉及的IP是虚构的IP&#xff0c;请替换成自己云服务器的IP&#xff0c;如有雷同&#xff0c;纯属巧合。 大致分为三部分&#xff1a; 一、安装docker 二、安装m…

cisco网络安全技术第3章测试及考试

测试 使用本地数据库保护设备访问&#xff08;通过使用 AAA 中央服务器来解决&#xff09;有什么缺点&#xff1f; 试题 1选择一项&#xff1a; 必须在每个设备上本地配置用户帐户&#xff0c;是一种不可扩展的身份验证解决方案。 请参见图示。AAA 状态消息的哪一部分可帮助…

<Project-11 Calculator> 计算器 0.2 工时计算器 WorkHours Calculator HTTP + JS

灵感 给工人发工资是按小时计算的&#xff0c;每次都要上网&#xff0c;我比较喜欢用 Hours Calculator &#xff0c;也喜欢它的其它的功能&#xff0c; 做个类似的。 我以为是 Python&#xff0c;结果在学 javascript 看 HTML&#xff0c;页面的基础还停留在 Frontpage 2000…

Cloudlog delete_oqrs_line 未授权SQL注入漏洞复现

0x01 产品简介 Cloudlog 是一个自托管的 PHP 应用程序,可让您在任何地方记录您的业余无线电联系人。使用PHP和MySQL构建的基于Web的业余无线电记录应用程序支持从HF到微波的一般站记录任务 0x02 漏洞概述 Cloudlog delete_oqrs_line 接口存在未授权SQL注入漏洞,未经身份验…

Marin说PCB之GMSL2 的Layout走线的注意事项

昨天有一位铁粉私信问我能不能讲解一下GMSL走线的一些注意事项啥的&#xff0c;我说当等我从以色列出差回来就给你更新一下这个&#xff0c;当然后来又很多的热心的粉丝提出很多的想法&#xff0c;我会一一给大家解答分享的&#xff0c;本期文章主要先给大家分享一下美信的手册…

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json

[Python学习日记-50] Python 中的序列化模块 —— pickle 和 json 简介 pickle 模块 json 模块 pickle VS json 简介 什么叫序列化&#xff1f; 序列化指的是将对象转换为可以在网络上传输或者存储到文件系统中的字节流的过程。序列化使得对象可以被保存、传输和恢复&#…

机器学习与神经网络:科技的星辰大海

前提 近日&#xff0c;2024年诺贝尔物理学奖颁发给了机器学习与神经网络领域的研究者&#xff0c;这是历史上首次出现这样的情况。这项奖项原本只授予对自然现象和物质的物理学研究作出重大贡献的科学家&#xff0c;如今却将全球范围内对机器学习和神经网络的研究和开发作为了一…

基于K8S的StatefulSet部署mysql主从

StatefulSet特性 StatefulSet的网络状态 拓扑状态&#xff1a;应用的多个实例必须按照某种顺序启动&#xff0c;并且必须成组存在&#xff0c;例如一个应用中必须存在一 个A Pod和两个B Pod&#xff0c;且A Pod必须先于B Pod启动的场景 存储状态&#xff1a;应用存在多个实例…

ChatGPT01-preivew体验报告:内置思维链和多个llm组合出的COT有啥区别呢?丹田与练气+中学生物理奥赛题测试,名不虚传还是名副其实?

一个月前&#xff0c;o1发布的时候&#xff0c;我写了篇文章介绍 逻辑推理能力堪比博士生&#xff0c;OpenAI发布全新AI模型系列&#xff1a; o1 - 大模型或许进入新阶段&#xff0c;还翻译了官方的介绍 解密OpenAI o1是如何让LLMs获得逻辑推理能力的 - CoT * RL&#xff0c;也…

【Linux】多线程安全之道:互斥、加锁技术与底层原理

目录 1.线程的互斥 1.1.进程线程间的互斥相关背景概念 1.2.互斥量mutex的基本概念 所以多线程之间为什么要有互斥&#xff1f; 为什么抢票会抢到负数&#xff0c;无法获得正确结果&#xff1f; 为什么--操作不是原子性的呢&#xff1f; 解决方式&#xff1a; 2.三种加锁…

基于SpringBoot+Vue的厨艺交流系统的设计与实现(源码+定制开发)厨艺知识与美食交流系统开发、在线厨艺分享与交流平台开发、智能厨艺交流与分享系统开发

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

第五届人工智能与教育国际学术会议(ICAIE 2024)

文章目录 一、会议详情二、重要信息三、大会介绍四、出席嘉宾五、征稿主题六、咨询 一、会议详情 二、重要信息 大会官网&#xff1a;https://ais.cn/u/vEbMBz提交检索&#xff1a;EI Compendex、IEEE Xplore、Scopus 三、大会介绍 第五届人工智能与教育国际学术会议&#x…

java逻辑运算符 C语言结构体定义

1. public static void main(String[] args) {System.out.println(true&true);//&两者均为true才trueSystem.out.println(false|false);// | 两边都是false才是falseSystem.out.println(true^false);//^ 相同为false&#xff0c;不同为trueSystem.out.println(!false)…