线程安全之锁的原理

news2024/11/14 22:09:02

       🔥🔥 欢迎来到小林的博客!!
      🛰️博客主页:✈️林 子
      🛰️博客专栏:✈️ Linux
      🛰️社区 :✈️ 进步学堂
      🛰️欢迎关注:👍点赞🙌收藏✍️留言

这里写目录标题

  • 线程安全
  • 互斥锁的使用
    • 全局锁的使用
    • 局部锁的使用
  • 互斥锁的原理
    • 竞争锁的过程
    • 释放锁的过程

线程安全

在多线程情况下,如果有临界资源且没有保护临界资源的情况下。线程是不安全的。因为CPU的调度机制是随机的,而不是等你一个线程执行完才去执行另一个线程。可能在你这个线程执行到一半时,又切换到了另一个线程执行。

我们可以用下面这段代码来验证

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

#define THREAD_MAX_NUM 5

int tickets = 10000;

void* ThreadRun(void* args)
{
  int id = *(int*)args;
  delete (int*)args;

  //抢票逻辑
  while(1)
  {
    if(tickets > 0)
    {
      usleep(1000);//延迟1000微秒
      tickets--; 
      printf("thread %d 抢了一张票。还剩 %d 张票\n",id,tickets);
    }else break;
  }
  return nullptr;
}

int main()
{
  pthread_t tids[THREAD_MAX_NUM];
  for(int i = 0 ; i < THREAD_MAX_NUM ; i++)
  {
    int* id = new int(i+1);
    pthread_create(tids+i , nullptr,ThreadRun,(void*)id);
  }
  
  for(int i = 0 ;  i < THREAD_MAX_NUM ; i++)
  {
    pthread_join(tids[i],nullptr);
  }
  return 0;
}

这段代码的逻辑就是创建5个线程,主线程等待5个线程。然后派这5个线程去抢10000张票(全局变量tickets)。 当票没了的时候跳出循环退出线程。

我们来看看运行结果:

第一次测试:

在这里插入图片描述

第二次测试:

在这里插入图片描述

第三次测试:

在这里插入图片描述

我们可以发现,三次测试结果。每次最后抢票都抢到了负数。这是非常危险的,如果在实际应用中,你只有100张票,却卖了105张。那么就会有5个用户没有座位,这影响是非常严重的。所以说,这个线程是不安全的。

为什么会这样呢?

我假设有2个线程,线程A和线程B。线程A先执行抢票,因为抢票的 tickets–并不是原子的。这条语句实际上是由三条汇编语句组成。 分别是: CPU加载tickets -> CPU对tickets进行减操作 -> CPU把tickets写回到内存。 而在这三个步骤中。如果在第二步完成,还没有走到第三步的时候。切换到了另一个线程B执行这段代码,线程B抢了5000票后,CPU又切到了线程A。并恢复线程A的上下文。可是在线程A的上下文中,tickets是9999。随后线程A又把9999写回到内存。 所以线程B明明已经抢了5000张票,又被线程A改回到了9999。这是非常非常不安全的。

在这里插入图片描述

如何保证线程安全呢?

我们只要让临界资源每次只能被一个执行流访问即可。即使是CPU调度切换,那么也要把没有访问权限的线程卡在临界资源之外。直到有访问权限的线程访问完临界资源之后。其他线程才能重新争夺这个访问权限。而这个访问权限,我们称它为

互斥锁的使用

我们可以用互斥锁来保护一段区域,这段区域被称为临界区

临界区的资源每次只能被一个执行流访问!

而互斥锁是一个pthread_mutex_t 类型的变量。

通过pthread_mutex_init函数初始化,pthread_mutex_destroy销毁。

pthread_mutex_t mtx; //创建锁变量

int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);//锁的初始化,返回0为成功。传入锁的地址和锁的属性

int pthread_mutex_destroy(pthread_mutex_t *mutex); //锁的销毁,返回0成功,传入锁的地址。 

当然,如果你想用一个全局的,或者静态的锁。你可以用PTHREAD_MUTEX_INITIALIZER这个宏来为锁初始化。
用法:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

锁创建和初始化之后,我们可以用pthread_mutex_lock 函数来加锁(本质是竞争锁,因为多个线程只能有一个线程持有锁)。 pthread_mutex_unlock函数来解锁。

加锁到解锁中间的区域,就是临界区。临界区只能同时被一个执行流访问!

int pthread_mutex_lock(pthread_mutex_t *mutex); //竞争锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁

全局锁的使用

那么我们就用 PTHREAD_MUTEX_INITIALIZER 来初始化全局锁演示一下。

加锁后的代码:

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

#define THREAD_MAX_NUM 5

int tickets = 1000;

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;  //创建全局锁

void* ThreadRun(void* args)
{
  int id = *(int*)args;
  delete (int*)args;

  //抢票逻辑
  while(1)
  {
    //抢票这段资源为临界资源,我们为其加锁
    pthread_mutex_lock(&mtx);
    if(tickets > 0)
    {
      usleep(1000);//延迟1000微秒
      tickets--; 
      printf("thread %d 抢了一张票。还剩 %d 张票\n",id,tickets);
      pthread_mutex_unlock(&mtx); //解锁 
    }else 
    {
      //这里也必须解锁。如果上面条件不成立,那么就要在这里进行解锁。
      pthread_mutex_unlock(&mtx); //解锁 
      break; 
    }
  }
  return nullptr;
}

int main()
{
  pthread_t tids[THREAD_MAX_NUM];
  for(int i = 0 ; i < THREAD_MAX_NUM ; i++)
  {
    int* id = new int(i+1);
    pthread_create(tids+i , nullptr,ThreadRun,(void*)id);
  }
  
  for(int i = 0 ;  i < THREAD_MAX_NUM ; i++)
  {
    pthread_join(tids[i],nullptr);
  }
  return 0;
}

我们为这个线程加上了互斥锁。那么我们来运行看看这段代码。

第一次测试:

在这里插入图片描述

第二次测试:

在这里插入图片描述

第三次测试:

在这里插入图片描述

加锁之后我们可以明显的感觉到,票数不会再被买到负数了,这是能够保证线程安全的。但同样的,速度也降低了很多。没加锁时的购票速度是很快的,而加锁后的速度却变慢了很多。所以加锁也是有消耗的(主要在于临界区只能同时被单个执行流访问)。

局部锁的使用

局部互斥锁的函数上面已经介绍过了,我们直接使用即可。但是要注意的是,如果要把锁和id同时传给线程的话。我们最好指定一个ThreadData类。用来存储线程的数据。随后把这个类对象传给线程。

代码演示:

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

#define THREAD_MAX_NUM 5

int tickets = 10000;

class ThreadData
{
  public:
    ThreadData(int uid,pthread_mutex_t* mtx):_uid(uid),_mtx(mtx){}
  public:
    int _uid;
    pthread_mutex_t* _mtx;
};

void* ThreadRun(void* args)
{
  ThreadData* data = (ThreadData*)args;

  //抢票逻辑
  while(1)
  {
    //抢票这段资源为临界资源,我们为其加锁
    pthread_mutex_lock(data->_mtx);
    if(tickets > 0)
    {
      usleep(1000);//延迟1000微秒
      tickets--; 
      printf("thread %d 抢了一张票。还剩 %d 张票\n",data->_uid,tickets);
      pthread_mutex_unlock(data->_mtx); //解锁 
    }else 
    {
      //这里也必须解锁。如果上面条件不成立,那么就要在这里进行解锁。
      pthread_mutex_unlock(data->_mtx); //解锁 
      break; 
    }
  }
  return nullptr;
}

int main()
{
  //创建局部锁 
  pthread_mutex_t mtx; 
  //初始化局部锁 
  pthread_mutex_init(&mtx,nullptr);
  pthread_t tids[THREAD_MAX_NUM];
  for(int i = 0 ; i < THREAD_MAX_NUM ; i++)
  {
    ThreadData* data = new ThreadData(i+1,&mtx);
    pthread_create(tids+i , nullptr,ThreadRun,(void*)data);
  }
  
  for(int i = 0 ;  i < THREAD_MAX_NUM ; i++)
  {
    pthread_join(tids[i],nullptr);
  }
  //销毁锁
  pthread_mutex_destroy(&mtx);
  return 0;
}

代码的运行结果和上面的全局互斥锁是一样的。

在这里插入图片描述

互斥锁的原理

我们都知道锁可以保证临界资源的安全。但是,锁也是被所有线程共享的。锁也是临界资源!!既然锁也是临界资源,那么锁如何保证自己是安全的?

就好比说一个1.5米的小瘦子说要保护一个2.0米的大胖子一样,小瘦子凭什么说能保证大胖子的安全?这时候小瘦子掏出了一把AK-47…大胖子才相信他能保护自己的安全…

这里有一份pthread_mutex_lock函数和pthread_mutex_unlock函数的伪代码

lock:
	movb $0, %al
	xchgb %al, mutex 
	if(al寄存器的内容 > 0)
	{
		return 0;
	}else 
		挂起等待;
	goto lock;
unlock: 
	movb $1, mutex 
	唤醒等待mutex的线程;
	return 0;

xchgb是一条汇编指令,意思是交换两个数的值。

而互斥锁的实现原理就是用一条汇编指令,将%al寄存器的内容与mutex的内容进行交换。

接下来我将图文演示申请锁到释放锁的整个过程。

竞争锁的过程

在这里插入图片描述

首先,看哪个线程先调用pthread_mutex_lock,假设线程B先调用。

在这里插入图片描述

这时候已经在线程B中调用了pthread_mutex_lock函数。执行第一句代码 movb $0, %al ,将0写入到寄存al中。

在这里插入图片描述

随后执行第二条指令xchgb %al, mutex 把al寄存器与mutex的内容进行交换。

在这里插入图片描述

突然,这时候CPU要调度线程A了。那么CPU把当前线程B运行的数据保存到线程B的上下文。也就是al寄存器的内容也会被保存到线程B的上下文。随后调度线程A。

在这里插入图片描述

调度线程A之后,在A调用了pthread_mutex_lock函数之后,执行第一条汇编语句 movb $0, %al,把0写入到al寄存器中。

在这里插入图片描述

随后调用第二条汇编语句 xchgb %al, mutex 把寄存器的值与mutex的值进行交换。

在这里插入图片描述

随后线程A继续往后执行if(al寄存器的内容 > 0) ,不满足条件,保存了自己的上下文之后被CPU挂起等待。

此时CPU又切换到了线程B。

在这里插入图片描述

CPU切换到线程B后,线程B把保存的上下文交给了CPU,所以此时al寄存器的内容被线程B替换为了1。

在这里插入图片描述

随后线程B继续往后执行,if(al寄存器的内容 > 0) 条件为真,最后pthread_mutex_lock返回0。返回后执行的就是临界区的代码,也就是我们写的抢票逻辑。在这期间不管哪个线程被调度,当xchgb %al, mutex这条指令被执行时。al的结果都不可能为1。因为mutex在第一次交换之后 ~ 锁释放之前会一直为0。所以,第一个 执行xchgb %al, mutex 这条与mutex进行交换的汇编指令的线程将会获得临界区的访问权限,也就是竞争锁成功!而竞争锁成功后,在释放锁之前,这段时间临界区只有竞争锁成功的线程可以访问。所以这就保证了线程安全。

因为 xchgb %al, mutex 的操作是原子的。所以锁也是原子的,因为只有这一句指令才是真正的竞争锁。即使在调用了 movb $0, %al这条汇编之后,线程被切换。也无法影响什么,因为锁只有一个,只能让一个人换走。

释放锁的过程

释放锁的过程很简单,先把mutex的值恢复即可。

在这里插入图片描述

然后再把所有挂起的线程唤醒

在这里插入图片描述

注意!线程B在退出pthread_mutex_lock函数的时候,对应保存的al寄存器的上下文就已经不存在了!!不要认为线程B在执行临界资源代码时,上下文还保存着 al = 1这个字段。这个字段在退出pthread_mutex_lock函数时就已经不存在了。是保存的上下文没了,不是al寄存器没了!!!切记切记不要弄混。

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

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

相关文章

思维模型 鸟笼效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。 1 鸟笼效应的应用 1.1 购物中的鸟笼效应 1 漂亮鞋子的故事 假设一个人在商场看到一双漂亮的鞋子&#xff0c;并冲动地购买了它们。当他回到家后&#xff0c;他发现这双鞋子并不适合他的…

【QT】QListWidget

新建项目 list widget&#xff0c;做了布局 #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);// listWidget的使用&#xff0c;基于itemQListWidgetItem* item …

【电子通识】USB发展历史

USB接口自1994年推出以来&#xff0c;经过29年的发展&#xff0c;经过USB1.0/1.1、USB2.0、USB3.x&#xff0c;发展到了现在的USB4&#xff1b;传输速率也从最开始的1.5Mbps&#xff0c;大幅提高到了最新的40Gbps。 USB1.0 1996年1月15日推出USB1.0接口规范规定低速传输速率为…

数字孪生软件架构选BS还是CS?不,我们选择CSaaS!

BS&#xff08;Browser/Server&#xff09;和CS&#xff08;Client/Server&#xff09;是两种不同的软件架构模式&#xff0c;具有不同的特点和优缺点。 BS&#xff08;Browser/Server&#xff09;架构 BS架构指的是基于浏览器和服务器的软件架构&#xff0c;客户端通常是一个…

1814_ChibiOS中的时间以及时间间隔处理

全部学习汇总&#xff1a; GreyZhang/g_ChibiOS: I found a new RTOS called ChibiOS and it seems interesting! (github.com) 1. 时间的相关配置&#xff0c;有tick的计数精度、时钟频率、间隔时间精度、时间类型大小等不同的配置。这些参数&#xff0c;涉及到系统的时间计数…

JVMGC复习

TLAB:默认给每一个线程开辟一块内存空间存放线程自己的对象 Class对象是存放在堆区的&#xff0c;不是方法区&#xff0c;类的元数据元数据并不是类的Class对象&#xff0c;Class对象是加载的最终产品&#xff0c;类的方法代码&#xff0c;变量名&#xff0c;方法名&#xff0c…

解决windows中被占端口问题(实测有效)

1、用管理员身份打开cmd 2、输入命令查找所被占的端口号 例&#xff1a;8902 netstat -ano | findstr 8902终结被占端口号的进程 例&#xff1a;端口号为8080&#xff0c;则查找到的pid为18524 taskkill /t /f /pid 18524强制&#xff08;/F参数&#xff09; 子进程&#x…

小公司如何成功申请企业邮箱

对于小公司来说拥有专业的企业邮箱不仅有助于提升公司形象&#xff0c;还能有效提高工作效率。小公司怎么申请企业邮箱&#xff1f;以下是一些步骤和建议供您参考。 需要明确公司的需求。 这包括确定所需用户账户的数量&#xff08;一般是目前使用人数再加上几个备用的邮箱&…

Docker入门到精通教程

Docker是什么 Docker是一个开源的应用容器引擎&#xff0c;它基于Go语言并遵从Apache2.0协议开源。容器技术是和我们的宿主机共享硬件资源及操作系统&#xff0c;实现资源的动态分配&#xff0c;在资源受到隔离的进程中运行应用程序及其依赖关系。 Docker可帮助更快地打包、测…

Redis实现附近商户

GEO数据结构的基本用法 GEO就是Geolocation的简写形式&#xff0c;代表地理坐标。Redis在3.2版本中加入了对GEO的支持&#xff0c;允许存储地理坐标信息&#xff0c;帮助我们根据经纬度来检索数据。常见的命令有&#xff1a; GEOADD&#xff1a;添加一个地理空间信息&#xf…

【广州华锐互动】VR石油钻井井控实训系统

在过去的几十年中&#xff0c;石油工业的发展速度一直在加快。为了适应这个快速发展的行业&#xff0c;需要新的技术和工具&#xff0c;而VR&#xff08;虚拟现实&#xff09;技术正是其中之一。本文将探讨VR石油钻井井控实训系统在石油工业教育中的应用。 在真实的钻井环境中&…

接口管理神器Apipost

自诞生以来&#xff0c;Apipost凭借其简洁直观的用户界面、强大的功能以及简单、易上手的操作&#xff0c;让Apipost成为了开发人员不可或缺的工具。本文将详细介绍Apipost的主要功能和使用方法&#xff0c;帮助大家更好地了解这款优秀的API开发工具。 下载安装 直接进入Apip…

Stable Diffusion WebUI扩展a1111-sd-webui-tagcomplete之Booru风格Tag自动补全功能详细介绍

安装地址 直接附上地址先: Ranting8323 / A1111 Sd Webui Tagcomplete GitCodeGitCode——开源代码托管平台,独立第三方开源社区,Git/Github/Gitlabhttps://gitcode.net/ranting8323/a1111-sd-webui-tagcomplete.git上面是GitCode的地址,下面是GitHub的地址,根据自身情…

个人微信CRM客户管理系统怎么选?功能介绍

现在市面上有许多种类的个人微信CRM客户管理系统可供选择&#xff0c;因此&#xff0c;我们需要选择最适合自己需求的微信管理系统CRM&#xff0c;最重要的是根据您的需求和期望的功能来进行筛选。 如何选择适合自己的微信CRM客户管理系统&#xff1f; 现在市面上的系统五花八…

Cloud Studio连接MySQL,Access denied for一系列问题

官方文档有写如何安装Mysql $ apt update $ apt install mysql-server mysql-client -y$ service mysql start mysql -uroot -p123456进入MySQL命令行 问题出在连接数据库这一步&#xff0c;命令行能进去&#xff0c;但是数据库插件和代码都连不上 Access denied for 大概率…

基于SSM的外卖点餐系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue、HTML 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是…

python实现列表倒叙打印

嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 def func(listNode):listNode.reverse()for i in listNode:print(i)li [1,2,3,4,5] func(li)利用python列表函数reverse&#xff08;&#xff09;将列表倒叙&#xff0c;然后遍历打印&#xff0c;但是这有一个缺点就是改变了…

固态硬盘的日常注意事项

固态硬盘是一种高速、低功耗、无噪音、抗震动的存储设备&#xff0c;但是在使用过程中也需要注意以下几点&#xff1a; 避免频繁重复写入同一块区域&#xff0c;这会缩短固态硬盘的使用寿命。定期清理垃圾文件和临时文件&#xff0c;以免占用过多的存储空间。避免在固态硬盘上…

RISC-V架构——物理内存属性和物理内存保护

1、物理内存属性&#xff08;PMA&#xff1a;Physical Memory Attributes&#xff09; &#xff08;1&#xff09;系统内存映射包含各种不同属性的地址空间范围&#xff0c;每个地址空间范围支持的操作不一样&#xff1b; &#xff08;2&#xff09;物理内存属性一般是在芯片设…

使用IDEA2022.1创建Maven工程出现卡死问题

使用IDEA创建Maven工程出现卡死问题&#xff0c;这个是一个bug 这里是别人和官方提供这个bug,大家可以参考一下 话不多说&#xff0c;上教程 解决方案&#xff1a; 方案1&#xff1a;更新idea版本 方案2&#xff1a;关闭工程&#xff0c;再新建&#xff0c;看图