线程安全之锁的原理

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

这里写目录标题

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

线程安全

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

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

#include<iostream>
#include<pthread.h> 
#include<unistd.h>#define THREAD_MAX_NUM 5int 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 5int 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 5int 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, %alxchgb %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.mzph.cn/news/110872.shtml

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

相关文章

1019hw

登录窗口头文件 #ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QToolBar> #include <QMenuBar> #include <QPushButton> #include <QStatusBar> #include <QLabel> #include <QDockWidget>//浮动窗口…

node+vue+mysql后台管理系统

千千博客系统&#xff0c;该项目作为一套多功能的后台框架模板&#xff0c;适用于绝大部分的后台管理系统开发。基于 vue.js&#xff0c;使用 vue-cli3 脚手架&#xff0c;引用 Element UI 组件库&#xff0c;数据库直连mysql方便开发快速简洁好看的组件。 功能包含如下&#…

UGUI交互组件InputField

一.InputField的结构 对象说明InputField挂有TextMeshPro-InputField组件的主体对象Text Area文本显示区Placeholder未输入时占位文本Enter text...Text输入的显示文本 二.InputField的属性 属性说明Text ViewportText Area子对象的引用Text ComponentText子对象的引用Text输入…

OpenP2P实现内网穿透远程办公

OpenP2P是一个开源、免费、轻量级的P2P共享网络。你的设备将组成一个私有P2P网络&#xff0c;里面的设备可以直接访问其它成员&#xff0c;或者通过其它成员转发数据间接访问。如果私有网络无法完成通信&#xff0c;将会到公有P2P网络寻找共享节点协助通信。 相比BT网络用来共享…

IOday8

#include <head.h>//要发送数据的结构体类型 struct msgbuf {long mtype; /* 消息类型*/char mtext[1024]; /* 正文数据 */}; //宏定义正文大小 #define SIZE sizeof(struct msgbuf)-sizeof(long) int main(int argc, const char *argv[]) {key_t key;if((keyft…

为什么产品经理都要考NPDP?

最近很多宝子问我&#xff0c;产品经理适合考什么证书&#xff1f;那必然是NPDP啊&#xff01;作为国际产品专业认证&#xff0c;NPDP证书是现如今最炙手可热且含金量相对较高的证书了&#xff0c;下面胖圆讲给大家详细介绍一下NPDP证书的具体信息。 1&#xff09;NPDP是什么&…

Simulink 最基础教程(三)常用模块

3.1源模块 1&#xff09;clock 这个模块的输出是 y(t)t。很多信号都是和时间 t 相关的&#xff0c;例如正弦波信号&#xff0c;可以写成 sin(w*t) 的形式。虽然软件也提供了正弦波模块&#xff0c;但如果用 clock 模块三角运算模块&#xff0c;对初学者而言&#xff0c;也是很好…

​蔚来自动驾驶,从 2020 年开始讲起的故事

2020 年底&#xff0c;摆脱 2019 年阴霾的李斌先生&#xff0c;热情而兴奋&#xff0c;再一次说&#xff1a;「欢迎来到蔚来日。」 那天蔚来发布了令人咋舌的智能驾驶硬件系统&#xff0c;4 块当时甚至还没有宣布量产日期的 Orin 芯片&#xff0c;11 路高清摄像头。 早在 ET7…

云服务器ip使用细节(公网、私有)

场景&#xff1a; 当我们对tcp服务器进行监听的时候&#xff0c;可能需要用到ip地址&#xff0c;比如使用httplib::Service::listen(ip, port)&#xff0c;而当我们访问tcp服务器时也需要ip地址 但这两个ip是不同的&#xff01; 每个云服务器通常都会有一个公网IP地址和一个私有…

Linux进阶-ipc共享内存

目录 共享内存 shmget()&#xff1a;创建或获取共享内存 shmat()&#xff1a;映射 shmdt()&#xff1a;解除映射 shmctl()&#xff1a;获取或设置属性 sem.h文件 sem.c文件 shm.c文件 Makefile文件 执行过程 共享内存 共享内存&#xff1a;将内存进行共享&#xff0c…

小程序中如何使用自定义组件应用及搭建个人中心布局

一&#xff0c;自定义组件 从小程序基础库版本 1.6.3 开始&#xff0c;小程序支持简洁的组件化编程。所有自定义组件相关特性都需要基础库版本 1.6.3 或更高。 开发者可以将页面内的功能模块抽象成自定义组件&#xff0c;以便在不同的页面中重复使用&#xff1b;也可以将复杂的…

02、MySQL-------主从复制

目录 七、MySql主从复制启动主从复制&#xff1a;原理&#xff1a;实现&#xff1a;1、创建节点2、创建数据库3、主从配置1、主节点2、从节点 4、测试&#xff1a;5、问题&#xff1a;1、uuid修改2、service_id3、读写不同步方法1&#xff1a;方法2&#xff1a; 七、MySql主从复…

pip install huggingface_hub时报错

pip install huggingface_hub时报错&#xff1a; 可以尝试&#xff1a;pip install --upgrade huggingface_hub 进行安装 方法参考了&#xff1a;https://blog.csdn.net/m0_72295867/article/details/132060750

HTTP基础

HTTP请求报文格式 HTTP 的请求报文分为三个部分 请求行&#xff08;Request Line&#xff09;、请求头&#xff08;Request Header&#xff09;和请求体&#xff08;Request Body&#xff09;。请求体是HTTP请求的核心&#xff0c;其中包含了需要上传服务器的数据。常见的请求…

macOS查端口占用进程

java开发人员&#xff0c;端口冲突的问题基本都遇到过吧&#xff01;以下的日志是否熟悉&#xff1a; *************************** APPLICATION FAILED TO START ***************************Description:The Tomcat connector configured to listen on port 8084 failed to …

小程序技术在信创操作系统中的应用趋势:适配能力有哪些?

小程序技术在信创操作系统中的应用前景非常广阔&#xff0c;但也面临着一些挑战和问题。开发者需要积极应对这些挑战和问题&#xff0c;为信创操作系统的发展和推广做出贡献。同时&#xff0c;开发者也需要关注小程序技术在信创操作系统中的应用趋势&#xff0c;积极探索新的应…

【Django 03】QuerySet 和 Instance应用

1. DRF QuerySet 和 Instance功能概述 1.1 QuerySet 从数据库中查询结果存放的集合称为 QuerySet。 Django ORM用到三个类&#xff1a;Manager、QuerySet、Model。每个Model都有一个默认的 manager实例&#xff0c;名为objects。Django的ORM通过Mode的objects属性提供各种数据…

Java 常用类(包装类)

目录 八大Wrapper类包装类的分类 装箱和拆箱包装类和基本数据类型之间的转换常见面试题 包装类方法包装类型和String类型的相互转换包装类常用方法&#xff08;以Integer类和Character类为例&#xff09;Integer类和Character类的常用方法 Integer创建机制&#xff08;面试题&a…

划词搜索IP插件

插件背景 浏览器插件可以让用户根据个人工作及日常需求来定制浏览器的功能和界面。当用户在网页上看到一些IP地址时&#xff0c;或许会好奇它们的来源和归属。传统的做法是&#xff0c;用户需要复制这个IP地址&#xff0c;然后跳转到埃文科技旗下的http://IPUU.net网站进行查询…

基于Java的考研信息查询系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09; 代码参考数据库参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者&am…