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

news2024/10/17 23:35:00

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

        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, 那么其他线程就拿不到锁!!

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

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

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

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

相关文章

使用OpenCV实现基于EigenFaces的人脸识别

引言 人脸识别技术近年来得到了飞速的发展&#xff0c;它被广泛应用于安全监控、门禁系统、智能设备等领域。其中&#xff0c;基于特征脸&#xff08;EigenFaces&#xff09;的方法是最早期且较为经典的人脸识别算法之一。本文将介绍如何使用Python和OpenCV库实现一个简单的人…

【LeetCode】每日一题 2024_10_15 三角形的最大高度(枚举、模拟)

前言 每天和你一起刷 LeetCode 每日一题~ LeetCode 启动&#xff01; 题目&#xff1a;三角形的最大高度 代码与解题思路 久违的简单题 这道题读完题目其实不难想到有两条路可以走&#xff1a; 1、题目很明显只有两种情况&#xff0c;枚举是第一个球是红球还是蓝球这两种情…

导数的概念及在模型算法中的应用

一. 导数概念与计算 1. 导数的物理意义&#xff1a; 瞬时速率。一般的&#xff0c;函数yf(x)在x处的瞬时变化率是 2. 导数的几何意义&#xff1a; 曲线的切线&#xff0c;当点趋近于P时&#xff0c;直线 PT 与曲线相切。容易知道&#xff0c;割线的斜率是当点趋近于 P 时&…

UE5学习笔记25-游戏中时间同步

一、原因 1.由于网络问题会导致服务器上的时间和客户端上获得的时间不一致 二、解决方法 在程序启动时向服务器请求服务器的时间返回给客户端并获得客户端发送消息的往返的时间&#xff0c;在获得客户端上的时间&#xff0c;用服务器上获得的时间加上往返时间减去客户端上的时…

稳字诀! 洞见 强者的社交格局:从不恋战——早读(逆天打工人爬取热门微信文章解读)

都是文字 引言Python 代码第一篇 洞见 强者的社交格局&#xff1a;从不恋战第二篇 稳字诀结尾 引言 今天很奇怪 一直都挺烦造的 好像有很多事情忙 但是就是忙着找不定 不能定下心来 主要还是在股市 其他方面应该没啥 计划表还是不够给力 没办法把心在约定住 稳字诀 勤燃香,奋…

深入 IDEA 字节码世界:如何轻松查看 .class 文件?

前言&#xff1a; 作为一名 Java 开发者&#xff0c;理解字节码对于优化程序性能、调试错误以及深入了解 JVM 运行机制非常重要。IntelliJ IDEA 作为最流行的开发工具之一&#xff0c;为开发者提供了查看 .class 文件字节码的功能。在本文中&#xff0c;我将带你一步步探索如何…

在 Spring 中使用 @EhCache 注解作为缓存

文章目录 项目概况项目设置一个简单的 RESTful Web 服务Spring 整合 EhCache第 1 步&#xff1a;更新依赖项以使用 EhCache Spring 注解第 2 步&#xff1a;设置自定义缓存管理器第 3 步&#xff1a;配置 EhCache第 4 步&#xff1a;测试缓存 刷新缓存总结推荐阅读文章 EhCache…

AD报错failed to add class member\net

什么原因导致的我到现在还没弄懂&#xff0c;总之解决方法是在PCB端删除所有现有的并且可删除的nets与components。下次问题复现了再补充截图&#xff08;不想再遇到了球球了这种玄学问题&#xff09;。 网络截图&#xff1a; 解决步骤&#xff1a;设计->类 把可删除的网络…

【论文翻译】HTVGNN:一种用于交通流量预测的混合时间变化图神经网络

题目A Novel Hybrid Time-Varying Graph Neural Network For Traffic Flow Forecasting论文链接https://arxiv.org/pdf/2401.10155v4关键词交通流预测&#xff0c;图神经网络&#xff0c;Transformer&#xff0c;多头自注意力 摘要 实时且精确的交通流量预测对于智能交通系统的…

OpenAI的Swarm是一个实验性质的多智能体编排框架

先上文档&#xff0c;然后解释&#xff0c;然后是代码 OpenAI的Swarm是一个实验性质的多智能体编排框架&#xff0c;旨在简化多智能体系统的构建、编排和部署。以下是对Swarm的详细介绍&#xff1a; 一、核心概念和特点 智能体&#xff08;Agent&#xff09;&#xff1a; Swar…

目标检测——Libra R-CNN算法解读

论文&#xff1a;Libra R-CNN: Towards Balanced Learning for Object Detection (2019.4.4) 作者&#xff1a;Jiangmiao Pang, Kai Chen, Jianping Shi, Huajun Feng, Wanli Ouyang, Dahua Lin 链接&#xff1a;https://arxiv.org/abs/1904.02701 代码&#xff1a;https://git…

计算机毕业设计 基于Python的汽车销售管理系统的设计与实现 Python毕业设计 Python毕业设计选题【附源码+安装调试】

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

Android Settings 设置项修改

Settings 设置项 在 Android 系统上,WRITE_SETTINGS 这个权限从 API 1 就已经开始有了。 通过在 app 中设置权限 android.permission.WRITE_SETTINGS 允许 app 读/写 系统设置。 在官方文档的描述中,还有一段注意事项: Note: If the app targets API level 23 or higher,…

live2d 实时虚拟数字人形象页面显示,对接大模型

live2dSpeek 测试不用gpu可以正常运行 https://github.com/lyz1810/live2dSpeek 运行的话还需要额外下载https://github.com/lyz1810/edge-tts支持语音 ## 运行live2dSpeek >npm install -g http-server >http-server . ## 运行edge-tts python edge-tts.py

前端布局与响应式设计综合指南(二)

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Css篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来Css篇专栏内容:前端布局与响应式设计综合指南(二) 目录 23、行内元素和块级元素&#xff1f;img算什么&…

【全网最全】AI产品经理面试高频100题答案解析

详细的目录如下&#xff0c;需要的小伙伴可以详细看一下~ 第一章&#xff1a;机器学习和深度学习的关系 第二章&#xff1a;机器学习7大经典算法 算法一&#xff1a;K近邻算法【分类算法】 1.1 KNN 算法的实现原理 1.2 KNN应用场景举例&#xff1a;预测候选人能不能拿到 O…

DVWA | Files Upload(文件上传)通关笔记

概念 **文件上传漏洞**是网络安全中常见的漏洞之一&#xff0c;攻击者可以利用该漏洞上传恶意文件&#xff0c;进而在服务器上执行恶意代码、绕过权限验证或获取敏感数据。文件上传漏洞主要发生在允许用户上传文件的Web应用程序中&#xff0c;比如图像、文档上传功能等。 ###…

3-3 AUTOSAR RTE 对SR Port的实现

返回总目录->返回总目录<- 目录 一、前言 二、显式访问 三、隐式访问 四、队列调用(Queued) 五、无效数据元素 一、前言 RTE作为SWC和BSW之间的通信机构,支持Sender-Receiver方式实现ECU内及ECU间的通信。 对于Sender-Receiver Port支持三种模式: 显式访问:若…

JavaScript全面指南(二)

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Javascript篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来JavaScript篇专栏内容:JavaScript全面指南(二) 目录 21、说明如何使用JavaScript提交表单&#xff1f; 2…

Java_EE(反射技术)

反射机制介绍: 什么是反射Java反射机制是Java语言一个很重要的特性&#xff0c;它使得Java具有了“动态性”。在Java程序运行时&#xff0c;对于任意的一个类&#xff0c;我们能不能知道这个类有哪些属性和方法呢&#xff1f;对于任意的一个对象&#xff0c;我们又能不能调用它…