Linux线程互斥锁

目录

🚩看现象,说原因

🚩解决方案

🚩互斥锁

 🚀关于互斥锁的理解

🚀关于原子性的理解

🚀如何理解加锁和解锁是原子的

🚩对互斥锁的简单封装


引言

大家有任何疑问,可以在评论区留言或者私信我,我一定尽力解答。

今天我们学习Linux线程互斥的话题。Linux同步和互斥是Linux线程学习的延伸。但这部分挺有难度的,请大家做好准备。那我们就正式开始了。

🚩看现象,说原因

我们先上一段代码:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<cassert>
using namespace std;
int NUM=5;
int ticket=1000;
class pthread
{
public:
     char buffer[1024];
     pthread_t id;
};
void *get_ticket(void *args)
{
     pthread *pth=static_cast<pthread*>(args);
     while(1)
     {
          usleep(1234);
          if(ticket<0)
          {
               return nullptr;
          }
          cout<<pth->buffer<<" is ruuing ticket: "<<ticket<<endl;
          ticket--;
          
     }
}

int main()
{
     vector<pthread*> pthpool;
     for(int i=0;i<NUM;i++)
     {
          pthread* new_pth=new pthread();
          snprintf(new_pth->buffer,sizeof (new_pth->buffer),"thread-%d",i+1);
          int n=pthread_create(&(new_pth->id),nullptr,get_ticket,new_pth);
          assert(n==0);
          (void)n;
          pthpool.push_back(new_pth);

     }
     for(int i=0;i<pthpool.size();i++)
     {
         int m= pthread_join(pthpool[i]->id,nullptr);
         assert(m==0);
         (void)m;
     }
     return 0;

}

这段代码模拟的是抢票模型,一共有一千张票,我们让几个线程同时去抢票。看看有什么不符合实际的情况发生。

还真有不符合实际的情况发生:竟然抢到了负票。卧槽,这是什么情况,我们赶紧分析一下。

首先,在代码中我们定义了一个全局变量:ticket 。这个变量被所有线程所共享。

对于这种情形,我们直接拉向极端情况:假设此时的票数只有一张了。一个线程进入if内部,但是对票数还没有进行操作,这时,时间片到了,这个线程被切了下去。紧接着,一个线程就通过if判断,顺利抢到了最后一张票,对票数进行了操作。此时已经无票可抢了。这时,那个被切下来的线程又带着它的数据开始了抢票。但是在这个线程看来,票数依旧还有最后一张,所以,它又对票数进行了减减操作,得到了负票。

这种情况显然是不合理的,假如一个电影院有100个座位,结果卖出去102张票,这怎么可以呢?

我们定义的全局变量,在没有保护的情况下,往往晒不安全的。像上面多个线程在交替执行时造成的数据安全问题,我们称之为出现了数据不一致问题

这就是个坑啊,必须解决。

🚩解决方案

在提出解决方案之前,我们先回顾几个概念。

  • 多个执行流进行安全访问的共享资源,叫做临界资源
  • 我们把多个执行流中,访问临界资源的代码叫做临界区,临界区往往是线程代码很小的一部分。
  • 想让多个线程串行访问共享资源的方式叫做互斥
  • 对一个资源进行访问的时候,要么不做,要么做完,这种特性叫做原子性。一个对资源进行的操作,如果只有一挑汇编语句完成,那么就是原子的,反之就不是原则的。这是当前我们对原子性的理解,后面还会发生改变。

 我们提出的解决方案就是加锁。相信大家第一次听到锁。对于什么是锁,如何加锁,锁的原理是什么我们都不清楚,别着急,我们在接下来的内容里会进行详细的详解。

我们先使用一下锁,见见猪跑!!

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<cassert>
using namespace std;
int NUM=5;
int ticket=1000;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
class pthread
{
public:
     char buffer[1024];
     pthread_t id;
};
void *get_ticket(void *args)
{
     pthread *pth=static_cast<pthread*>(args);
     while(1)
     {  
          pthread_mutex_lock(&mutex);
          usleep(1234);
        
          if(ticket<0)
          {  
               pthread_mutex_unlock(&mutex);
               return nullptr;
             
          }
          cout<<pth->buffer<<" is ruuing ticket: "<<ticket<<endl;
          ticket--;
          pthread_mutex_unlock(&mutex);
          
     }
}

int main()
{
     vector<pthread*> pthpool;
     for(int i=0;i<NUM;i++)
     {
          pthread* new_pth=new pthread();
          snprintf(new_pth->buffer,sizeof (new_pth->buffer),"user-%d",i+1);
          int n=pthread_create(&(new_pth->id),nullptr,get_ticket,new_pth);
          assert(n==0);
          (void)n;
          pthpool.push_back(new_pth);

     }
     for(int i=0;i<pthpool.size();i++)
     {
         int m= pthread_join(pthpool[i]->id,nullptr);
         assert(m==0);
         (void)m;
     }
     return 0;

}

 

结果显示抢票的过程非常顺利,接下来,我们把重心指向锁。

🚩互斥锁

 首先,我们先认识一些锁的常见接口

// 所有锁的相关操作函数都在这个头文件下
//这些函数如果又返回值,操作成功的话,返回0,失败的话。返回错误码。错误原因被设置
#include <pthread.h>
// 锁的类型,用来创建锁
pthread_mutex_t
// 对锁进行初始化,第二个参数一般设位null
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 如果这个锁没有用了,可以调用该函数对锁进行销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 如果创建的锁是全局变量,可以这样初始化。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 对特定代码部分进行上锁,这部分代码只能有一次只能有一个执行流进入,被保护的资源叫做临界资源。
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 尝试上锁,不一定成功。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 取消锁。
int pthread_mutex_unlock(pthread_mutex_t *mutex);

刚刚,我们已经使用一种方式实现了加锁,接下来,我们用另一种方式:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cassert>
using namespace std;
int NUM = 5;
int ticket = 1000;
class Thread_Data
{
public:
     Thread_Data(string name,pthread_mutex_t* mutex):_name(name),_mutex(mutex)
     {}
     ~Thread_Data()
     {}
public:
 string _name;
 pthread_mutex_t*  _mutex;
};
void *get_ticket(void *args)
{
     Thread_Data *pth = static_cast<Thread_Data*>(args);
     while (1)
     {
          pthread_mutex_lock(pth->_mutex); 
          if (ticket > 0)
          {  
               usleep(1234); 
               cout << pth->_name << " is ruuing ticket: " << ticket << endl;  
               ticket--;
               pthread_mutex_unlock(pth->_mutex); 
          } 
          else
          {
               pthread_mutex_unlock(pth->_mutex);
               break;
          }
     }
}

int main()
{
     pthread_mutex_t mutex;
     pthread_mutex_init(&mutex,nullptr);
     vector<pthread_t> tids(NUM); 
     for (int i = 0; i < NUM; i++)
     {
          char buffer[1024];
          Thread_Data *td=new Thread_Data(buffer,&mutex);
          snprintf(buffer, sizeof(buffer), "user-%d", i + 1);
           
           int n =pthread_create(&tids[i], nullptr, get_ticket, td);
          assert(n == 0);
          (void)n;
     }
     for (int i = 0; i < tids.size(); i++)
     {
          int m = pthread_join(tids[i], nullptr);
          assert(m == 0);
          (void)m;
     }
     return 0;
}

 

运行一下,发现一直是4号线程在跑,其他线程呢?我也没让其他线程退出呀!而且抢票的时间变长了。

  • 加锁和解锁是多个线程串行进行的,所以程序允许起来会变得很慢。
  • 锁只规定互斥访问,没有规定谁优先访问。
  • 锁就是让多个线程公平竞争的结果,强者胜出嘛。

 🚀关于互斥锁的理解

  • 所有的执行流都可以访问这一把锁,所以锁是一个共享资源。
  • 加锁和解锁的过程必须是原子的,不会存在中间状态。要么成功,要么失败。加锁的过程必须是安全的。
  • 谁持有锁,谁进入临界区。

 如果一个执行流申请锁成功,继续向后运行;如果申请失败的话,这个执行流怎么办?

这种情况试一试不就知道了。我们依旧使用上面的一份代码,稍稍做一下修改:

 

所以,当一个执行流申请锁失败时,这个执行流会阻塞在这里。


🚀关于原子性的理解

如图,三个执行流

 

问:如果线程1申请锁成功,进入临界资源,正在访问临界资源区的时候,其他线程在做什么?

 答:都在阻塞等待,直到持有锁的线程释放锁。

问; 如果线程1申请锁成功,进入临界资源,正在访问临界资源区的时候,可不可以被切换?

答:绝对是可以的,CPU管你有没有锁呢,时间片到了你必须下来。当持有锁的线程被切下来的时候,

是抱着锁走的,即使自己被切走了,其他线程依旧无法申请锁成功,也就无法继续向后执行。

这就叫作:江湖上没有我,但依旧有我的传说。

所以对于其他线程而言,有意义的锁的状态,无非两种:①申请锁前,②释放锁后

 所以,站在其他线程的角度来看待当前持有锁的过程,就是原子的。

 所以,未来我们在使用锁的时候,要遵守什么样的原则呢?

  • 一定要保证代码的粒度(锁要保护的代码的多少i)要非常小。
  • 加锁是程序员的行为,必须做到要加的话所有的线程必须要加锁。

🚀如何理解加锁和解锁是原子的

 在分析如何实现加锁和解锁之前,我们先形成几个共识:

  • CPU内执行流只有一套,且被所有执行流所共享。
  • CPU内寄存器的内容属线程所有,是每个执行流的上下文。时间片到达,数据带走。
  • 在进行加锁和解锁的时候,这个线程随时会因时间片已到而被换下来。

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性 。

如图:

我们假设有线程A,B两个线程,A想要获得锁 

锁内存储的数据就是int类型的1。 A线程中有数字0。

①:movb $0,%al:将线程A中的1move到寄存器中。此时,是有可能发生时间片到达的,但是寄存器内的数据属于线程A,线程A是要带走的。

②:xchgb %al,mutex:将锁中的数据和寄存器内的数据进行交换。此时寄存器内的数据变成1,锁中的数据变为0。这是关键的一步,也有可能会发生切换。假设不巧的很,A线程被切下去了,B线程被切上来了。B线程从第一步开始,走到现在,寄存器内的数据应该是0。然后进入判断体eles进行挂起等待。

③如果在第二步中线程A被切下来,等待一段时间,时间片再次轮到线程A时,A将自己的数据加载到寄存器内进入判断,然后获得锁。

交换的过程由一条汇编构成

交换的本质:共享的数据,交换到线程的上下文中。 

那么。如何完成解锁的操作呢。解锁的操作特别简单,只需一步。

将寄存器内的1归还给锁。然后return返回就可以了。

🚩对互斥锁的简单封装

相信大家对互斥锁都有了充分的了解。接下来,我们就实现一下对互斥锁的简单封装。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cassert>

class Mutex
{
public:
     Mutex(pthread_mutex_t *mutex) : _mutex(mutex)
     {
     }
     void unlock()
     {
          if (_mutex)
          {
                pthread_mutex_unlock(_mutex);
          }
     }
     void lock()
     {
         if(_mutex)
         {
               pthread_mutex_lock(_mutex);
         }
     }
     ~Mutex()
     {
     }

public:
     pthread_mutex_t *_mutex;
};
class Lockguard
{
public:
     Lockguard(Mutex mutex) : _mutex(mutex)
     {
          _mutex.lock();
     }
     ~Lockguard()
     {
          _mutex.unlock();
     }

public:
     Mutex _mutex;
};

这种利用变量出了函数作用域自动销毁的性质,我们称之为RAII特性。

到这里,我们本篇的内容也就结束了,我们期待下一期博客相遇。

 

 

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

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

相关文章

昇思25天学习打卡营第4天|onereal

今天学习的内容是&#xff1a;ResNet50迁移学习 以下内容拷贝至教程&#xff0c;实话实话看不懂&#xff0c;迷迷糊糊都运行jupyter里的代码。走完程序&#xff0c;训练生成了一些图片。 ResNet50迁移学习 在实际应用场景中&#xff0c;由于训练数据集不足&#xff0c;所以很少…

python OpenCV 库中的 cv2.Canny() 函数来对图像进行边缘检测,并显示检测到的边缘特征

import cv2# 加载图像 image cv2.imread(4.png)# 使用 Canny 边缘检测算法提取边缘特征 edges cv2.Canny(image, 100, 200)# 显示边缘特征 cv2.imshow(Edges, edges) cv2.waitKey(0) cv2.destroyAllWindows() 代码解析&#xff1a; 导入 OpenCV 库&#xff1a; import cv2加…

【十】【QT开发应用】QT中文乱码解决方案

QT中文乱码解决方案 粘贴代码导致的乱码 粘贴别人的代码时,在记事本里面"过一遍",然后再粘贴到QTCreator 使用u8 配置QT 不使用QT使用VS QT自选编码格式 结尾 最后&#xff0c;感谢您阅读我的文章&#xff0c;希望这些内容能够对您有所启发和帮助。如果您有任何问…

新能源汽车CAN总线故障定位与干扰排除的几个方法

CAN总线是目前最受欢迎的现场总线之一,在新能源车中有广泛应用。新能源车的CAN总线故障和隐患将影响驾驶体验甚至行车安全,如何进行CAN总线故障定位及干扰排除呢? 目前,国内机动车保有量已经突破三亿大关。由于大量的燃油车带来严峻的环境问题,因此全面禁售燃油车的日程在…

C语言笔记26 •顺序表应用•

基于动态顺序表实现通讯录项目 1.通讯录其实也就是顺序表&#xff0c;就是把里面存的数据类型变了一下 &#xff0c;所以有一些方法对于顺序表适用&#xff0c;对于通讯录也是适用的&#xff08;初始化&#xff0c;销毁&#xff0c;内存空间扩容&#xff09;。 2.要用到顺序表…

Ngnix内存池——高并发实现高效内存管理

目录 一、高并发下传统方式的弊端 1、常用的内存操作函数 2、弊端一 3、弊端二 4、弊端三 5、弊端四 二、弊端解决之道 1、内存管理维度分析 2、内存管理组件选型 三、高并发内存管理最佳实践 1、内存池技术 2、内存池如何解决弊端 3、高并发内存池如何实现 四、…

springboot+vue+mysql+mybatis 二手交易平台

springbootvuemysqlmybatis 二手交易平台 相关技术 javaspringbootmybatismysqlvueelementui

十常侍乱政 | 第2集 | 愿领精兵五千,斩关入内,册立新君,诛杀宦党,扫清朝廷,以安天下 | 三国演义 | 逐鹿群雄

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 &#x1f4cc;这篇博客是毛毛张分享三国演义文学剧本中的经典台词和语句&#xff0c;本篇分享的是《三国演义》第Ⅰ部分《群雄逐鹿》的第2️⃣集《十常侍乱政治》&am…

DigitalOcean Droplet 云主机新增内置第五代 Xeon CPU 机型

DigitalOcean 近期宣布&#xff0c;在其高级 CPU 服务器&#xff08;Premium CPU-Optimized Droplet&#xff09;队列中引入英特尔第五代Xeon可扩展处理器&#xff08;代号为 Emerald Rapids&#xff09;。作为英特尔产品线中的最新一代用于数据中心工作负载的处理器&#xff0…

干涉阵型成图参数记录【robust】

robust 这个玩意经常忘记&#xff0c;就是取2的时候是更加显示大尺度的结构&#xff0c;取-2更加显示小尺度结果&#xff0c;一般取0就是正常就好了

数学建模--lingo解决线性规划问题~~灵敏度分析的认识

目录 1.线性规划问题举隅 &#xff08;1&#xff09;问题介绍 &#xff08;2&#xff09;问题分析 &#xff08;3&#xff09;灵敏度分析 &#xff08;4&#xff09;方法缺陷 &#xff08;5&#xff09;可行域&凸集 2.lingo的简单认识 &#xff08;1&#xff09;默认…

Halcon 如何根据特征过滤区域和XLD

一 如何跟进特征过滤区域和XLD dev_open_window(0,0,512,512,black,WindowHandle)read_image(Image,fabrik)threshold(Image,Region,128,255)connection(Region,ConnectedRegions)*根据面积范围[8000,9000] dev_display(Image)select_shape(ConnectedRegions,SelectedRegions,…

数据结构速成--树和二叉树

由于是速成专题&#xff0c;因此内容不会十分全面&#xff0c;只会涵盖考试重点&#xff0c;各学校课程要求不同 &#xff0c;大家可以按照考纲复习&#xff0c;不全面的内容&#xff0c;可以看一下小编主页数据结构初阶的内容&#xff0c;找到对应专题详细学习一下。 气死了…

springboot + Vue前后端项目(第二十记)

项目实战第二十记 写在前面1. 高德地图官网2. 开发文档3. 集成高德地图3.1 在public文件夹下创建config.js3.2 index.html&#xff08;在项目启动文件中引入外部的js&#xff09;3.3 点标记&#xff08;用点标记当前位置&#xff09;3.4 信息窗体&#xff08;点击当前位置&…

简易深度学习(1)深入分析神经元及多层感知机

一、神经元 单个神经元结构其实可以认为是一个线性回归模型。例如下图中 该神经元输入为三个特征&#xff08;x1&#xff0c;x2&#xff0c;x3&#xff09;&#xff0c;为了方便理解&#xff0c;大家可以认为每条线上都有一个权重和特征对应&#xff08;w1&#xff0c;w2&…

麒麟系统安装MySQL

搞了一整天&#xff0c;终于搞定了&#xff0c;记录一下。 一、背景 项目的原因&#xff0c;基于JeecgBoot开发的系统需要国产化支持&#xff0c;这就需要在电脑上安装MySQL等支撑软件。 国产化项目的操作系统多是麒麟系统&#xff0c;我的系统如下&#xff1a; arm64架构。…

ISSCC论文详解2024 34.2——双端口设计实现高面积利用的浮点/整数存算

本文将要介绍的文献主题为浮点存内计算&#xff0c;题目为《A 16nm 96Kb Integer/Floating-Point Dual-Mode-Gain-CellComputing-in-Memory Macro Achieving 73.3-163.3TOPS/W and 33.2-91.2TFLOPS/W for AI-Edge Devices》&#xff0c;下面本文将从文章基本信息与背景知识、创…

5.9k!一款清新好用的后台管理系统!【送源码】

今天给大家分享的开源项目是一个优雅清新后台管理系统——Soybean Admin。 简介 官方是这样介绍这个项目的&#xff1a; Soybean Admin 使用的是Vue3作为前端框架&#xff0c;TypeScript作为开发语言&#xff0c;同时还整合了NaiveUI组件库&#xff0c;使得系统具有高可用性和…

分页处理封装+分页查询题目列表

文章目录 1.sun-club-common封装分页1.com/sunxiansheng/subject/common/eneity/PageInfo.java2.com/sunxiansheng/subject/common/eneity/PageResult.java 2.sun-club-application-controller1.SubjectInfoDTO.java 继承PageInfo并新增字段2.SubjectController.java 3.sun-clu…

信息学奥赛初赛天天练-37-CSP-J2021阅读程序-质数、合数、约数、约数个数、约数和、增加质因数对约数个数、约数和的影响

PDF文档公众号回复关键字:20240627 质数 质数和合数是数学中对于自然数&#xff08;不包括0和1&#xff09;的两种重要分类 质数 (Prime Number) 一个大于1的自然数&#xff0c;除了1和它本身以外不再有其他因数的数称为质数 例如 2、3、5、7、11、13、17、19等都是质数 …