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

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

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

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

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

相关文章

软件测试工程师面试整理 —— 操作系统与网络基础!

在软件测试中&#xff0c;了解操作系统和网络基础知识对于有效地进行测试工作至关重要。无论是在配置测试环境、调试网络问题&#xff0c;还是在进行性能测试和安全测试时&#xff0c;这些知识都是不可或缺的。 1. 操作系统基础 操作系统&#xff08;Operating System, OS&am…

OgreNext高级材质中增加线宽,点大小,虚线模式绘制支持

修改Ogre高级材质系统&#xff0c;增加线宽&#xff0c;点大小&#xff0c;虚线模式&#xff0c;虚线参数的支持,效果如下&#xff1a; 需要修改的代码文件如下&#xff1a; 修改如下 代码文本&#xff1a; //范围[0.2 - 51] 0.2 * [0,255];Ogre::uint8 mLineWidth;//范围[…

【数据结构】:破译排序算法--数字世界的秩序密码(二)

文章目录 前言一.比较排序算法1.Bubble Sort冒泡排序1.1.冒泡排序原理1.2.冒泡排序过程1.3.代码实现1.4.复杂度和稳定性 2.Quick Sort快速排序2.1递归快速排序2.1.1.递归快速排序原理2.1.2.递归快速排序过程2.1.3.代码实现 2.2.非递归快速排序2.2.1.非递归快速排序原理2.2.2.非…

MATLAB智能优化算法-学习笔记(5)——蚁群算法求解容量受限的车辆路径问题

蚁群算法在求解容量受限的车辆路径问题(Capacitated Vehicle Routing Problem, CVRP)中具有广泛应用。这类问题属于组合优化问题,涉及将若干辆具有容量限制的车辆,从配送中心出发为多个客户点提供服务,要求每辆车满足各客户的需求且总运载量不超过车辆容量,最终需要找到一…

python深浅拷贝,可变变量与不可变变量

赋值 在 python 中&#xff0c;赋值是将一个值或对象分配给一个变量的过程。赋值操作符是 &#xff0c;用于将右侧的值或对象赋给左侧的变量。 赋值&#xff1a;l2的值会随着原对象l1的值一同改变 l1 [1, 2, 3, 4] print(l1:, l1) l2 l1 print(l2:, l2) 给li列表新增元素 …

检测头篇 | 手把手教你如何去更换YOLOv8的检测头为ASFF_Detect

前言:Hello大家好,我是小哥谈。自适应空间特征融合(ASFF)的主要原理旨在解决单次检测器中不同尺度特征的不一致性问题。具体来说,ASFF通过动态调整来自不同尺度特征金字塔层的特征贡献,确保每个检测对象的特征表示是一致且最优的。本文所做出的改进是将YOLOv8的检测头更换…

使用 Spring 框架构建 MVC 应用程序:初学者教程

Spring Framework 是一个功能强大、功能丰富且设计精良的 Java 平台框架。它提供了一系列编程和配置模型&#xff0c;旨在简化和精简 Java 中健壮且可测试的应用程序的开发过程。 人们常说 Java 太复杂了&#xff0c;构建简单的应用程序需要很长时间。尽管如此&#xff0c;Jav…

论文翻译 | OpenICL: An Open-Source Framework for In-context Learning

摘要 近年来&#xff0c;上下文学习&#xff08;In-context Learning&#xff0c;ICL&#xff09;越来越受到关注&#xff0c;并已成为大型语言模型&#xff08;Large Language Model&#xff0c;LLM&#xff09;评估的新范式。与传统微调方法不同&#xff0c;ICL无需更新任何参…

龙信科技:引领电子物证技术,助力司法公正

文章关键词&#xff1a;电子数据取证、电子物证、手机取证、计算机取证、云取证、介质取证 在信息技术飞速发展的今天&#xff0c;电子物证在司法领域扮演着越来越重要的角色。苏州龙信信息科技有限公司&#xff08;以下简称“龙信科技”&#xff09;作为电子数据取证领域的先…

bat(批处理脚本学习)

输出banner echo off echo () echo JL echo ^|^| echo LJ echo _,--"""""""---. echo , …

从零实现高并发内存池

目录 1. 项目介绍1.1 这个项目具体功能是什么&#xff1f;1.2 本项目的知识储备 2. 什么是内存池2.1 池化技术2.2 内存池主要解决的问题2.3 malloc 3. 定长内存池设计4. 高并发内存池整体框架设计4.1 Thread Cache的设计思路4.2 Central Cache的设计思路4.3 Page Cache的设计思…

【C语言】分支结构switch

switch分支语句 多适用于明确表达式结果的情况&#xff0c;多个分支&#xff0c;用if过于繁琐。 case后跟具体的表达式值&#xff0c;break&#xff1b;跳出分支语句。 #include <stdio.h> #include <math.h> /* 功能&#xff1a;选择结构&#xff08;switch&…

Qt初识_项目文件解析

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 Qt初识_项目文件解析 收录于专栏【Qt开发】 本专栏旨在分享学习Qt的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 目录 1. pro文件解析 2.…

java异步多线程Async学习记录

java异步多线程Async学习记录 第1步:声明线程池AsyncConfiguration import org.springframework.context.annotation.Bean; import org.springframework

vue+element的confirm提示消息文字变色和换行

效果: 思路: 可以考虑采用模板字符串的思路实现 代码: this.confirm(您确定要<b style"Color: red">${text}</b>的数据项&#xff1f;<br/>单位名称: ${row.companyName} <br/>属性: ${row.attributeName}).then(() > {console.log(确定…

SCM供应商管理怎么做?

在企业的供应链管理中&#xff0c;供应商管理是至关重要的一环。然而&#xff0c;传统的供应商管理方式常常面临诸多痛点&#xff0c;导致管理效率低下、成本增加、风险增大。不注重供应商管理的企业&#xff0c;常常会面临以下问题&#xff1a; 供应商档案管理难&#xff1a;…

Redis 五种数据类型的操作命令

一、五种数据类型的介绍 五种数据类型如图所示&#xff1a; Redis 是一个开源的键值存储系统&#xff0c;它支持多种数据结构&#xff0c;每种数据结构都有其特定的用例和底层实现。以下是 Redis 的五种主要数据类型&#xff0c;以及它们适合存储的数据类型和底层实现&#xf…

健康生活的重要性

在当今快节奏的生活中&#xff0c;养生保健已成为人们日益关注的话题&#xff0c;而健身作为其中的重要一环&#xff0c;更是被赋予了前所未有的重视。谈及养生保健与健身&#xff0c;我们不得不深入思考&#xff1a;如何在繁忙的日常中&#xff0c;找到那条通往健康与活力的道…

MAC地址漂移实验

MAC地址漂移实验的概述&#xff1a; MAC地址漂移实验的概述主要围绕网络设备中的MAC地址动态变化及其检测与防护措施。以下是对MAC地址漂移实验的具体介绍&#xff1a; MAC地址漂移的定义&#xff1a;MAC地址漂移是指在同一个VLAN内&#xff0c;一个MAC地址被交换机的两个不同…

【哈希】1. leetcode 1. 两数之和

1 题目描述 题目链接&#xff1a;两数之和 2 题目解析 一般的思维&#xff1a;找到两个数A和B&#xff0c;判断A和B相加是否为target。 我们可以采用逆向思维&#xff1a;找到一个数A&#xff0c;在nums数组中找是否有值等于target - A&#xff0c;因为题目要求只返回一个…