【多线程中的线程安全问题】线程互斥

1 🍑线程间的互斥相关背景概念🍑

先来看看一些基本概念:

  • 1️⃣临界资源:多线程执行流共享的资源就叫做临界资源。
  • 2️⃣临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
  • 3️⃣互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

互斥量mutex:

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量(比如全局变量),会带来一些问题。

比如一个大家熟知的栗子:售票。我们用一个全局整形变量记录票的个数,多个线程并发的去抢票,我们不难写出下面这样的代码:

int g_ticket=10000;void* Run(void* args)
{string name=static_cast<const char*>(args);while(true){if(g_ticket<=0){break;}else{cout<<"I am "<<name<<",is running  tickets"<<g_ticket<<endl;g_ticket--;}usleep(2000);}return nullptr;
}int main()
{pthread_t ptids[5];for(int i=0;i<5;++i){char* name=new char[26];snprintf(name,26,"pthread%d",i+1);pthread_create(ptids+i,nullptr,Run,name);}for(int i=0;i<5;++i){pthread_join(ptids[i],nullptr);}return 0;
}

当我们运行时:
在这里插入图片描述
我们发现,有多个线程抢到了同一张票,并且打印混乱。有些情况下票还有可能变成了负数,而这就是线程不安全所带来的问题,解决办法我们在下面会给出详细解释。


2 🍑用互斥锁解决线程安全问题🍑

2.1 🍎分析问题 🍎

我们来分析下上面的代码为什么会出现那样的结果?

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • 减减ticket 操作本身就不是一个原子操作。

我们可以取出渐渐ticket取出ticket–部分的汇编代码:

objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

- - 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中;
  • update : 更新寄存器里面的值,执行-1操作;
  • store :将新值,从寄存器写回共享变量ticket的内存地址。

要解决以上问题,需要做到三点:

  • 1️⃣代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 2️⃣如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 3️⃣如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

2.2 🍎互斥量的接口 🍎

🍋初始化互斥量🍋

初始互斥量有两种方式:

  • 方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 方法2,动态分配:
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
const pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL

这两种方式选择哪一种都是OK的。

🍋销毁互斥量🍋

销毁互斥量需要注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁;
  • 不要销毁一个已经加锁的互斥量;
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
int pthread_mutex_destroy(pthread_mutex_t *mutex)

🍋互斥量加锁和解锁🍋

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用pthread_mutex_lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

所以我们可以改进下上面的抢票:

int g_tictet=10000;
pthread_mutex_t mtu=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
void* Run(void* args)
{string name=static_cast<const char*>(args);while(true){pthread_mutex_lock(&mtu);if(g_tictet<=0){pthread_mutex_unlock(&mtu);break;}else{cout<<"I am "<<name<<",is running  tickets"<<g_tictet<<endl;g_tictet--;}pthread_mutex_unlock(&mtu);usleep(2000);}return nullptr;
}int main()
{pthread_t ptids[5];for(int i=0;i<5;++i){char* name=new char[26];snprintf(name,26,"pthread%d",i+1);pthread_create(ptids+i,nullptr,Run,name);}for(int i=0;i<5;++i){pthread_join(ptids[i],nullptr);}return 0;
}

当我们再次运行时:
在这里插入图片描述
我们发现不会出现多个线程抢占同一张票并且打印混乱的情况了。

代码中值得注意的事情有:加锁的策略是:选用的粒度一般是越细越好

🍋互斥量实现原理探究🍋

搞了这么多,那么互斥量的实现原理究竟是啥捏?

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

我们可以自己实现一份lock和unlock的伪代码

lock:movb $0,%alxchgb %al,mutexif(al寄存器的内容>0)return 0;//表示申请锁成功else挂起等待;goto lock;unlock:movb $1,%al唤醒等待mutex的线程;return 0;//表示释放锁成功

通过上面的伪代码我们可以知道当初始值mutex的值为1时,假设线程1先进行申请锁,会先将寄存器中的值改为0,然后用寄存器中的0交换mutex中的1,此时1被线程1给拿到了,假设此时线程1的时间片到了,要切换线程2执行,在切换之前先保存了线程1的上下文数据,然后切换;此时线程2从头执行将寄存器中的数值改为0,然后交换,但是唯一的1已经被线程1给拿走了,所以线程而只有挂起等待;当重新切换回线程1的时候,线程1会重新恢复上下文数据,也就是寄存器的内容会被恢复到切换前,所以判断寄存器的内容>0,申请成功。此时我们发现就算是有多个线程并发的抢占锁资源时,也只有一个线程能够申请成功,其他线程在挂起等待,因为这里面的1只有一个,并且是以交换形式进行的,可以理解这里面的1本质就是一把锁。

释放资源就更好理解了,将寄存器的值修改为1,然后唤醒等待锁的线程即可。从释放锁的那段伪代码中我们也能够看到:当多个线程申请同一把锁时,一个线程申请了锁后,虽然其他线程不能够申请了,但是却可以释放该锁


2.3 🍎可重入VS线程安全 🍎

🍋概念🍋

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

🍋常见的线程不安全/安全的情况🍋

不安全情况:

  • 1️⃣不保护共享变量的函数
  • 2️⃣函数状态随着被调用,状态发生变化的函数
  • 3️⃣返回指向静态变量指针的函数
  • 4️⃣调用线程不安全函数的函数

安全情况:

  • 1️⃣每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 2️⃣类或者接口对于线程来说都是原子操作
  • 3️⃣多个线程之间的切换不会导致该接口的执行结果存在二义性

🍋常见不可重入/可重入的情况🍋

不可重入:

  • 1️⃣调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 2️⃣调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 3️⃣可重入函数体内使用了静态的数据结构

可重入:

  • 1️⃣不使用全局变量或静态变量
  • 2️⃣不使用用malloc或者new开辟出的空间
  • 3️⃣不调用不可重入函数
  • 4️⃣不返回静态或全局数据,所有数据都有函数的调用者提供
  • 5️⃣使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

🍋可重入与线程安全联系与区别🍋

联系:

  • 1️⃣函数是可重入的,那就是线程安全的
  • 2️⃣函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 3️⃣如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

区别:

  • 1️⃣可重入函数是线程安全函数的一种
  • 2️⃣线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 3️⃣如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

2.4 🍎死锁🍎

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

🍋死锁四个必要条件🍋

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

🍋避免死锁🍋

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

死锁避免算法有银行家算法和死锁检测算法,大家有兴趣可以自行下去研究。


3 🍑用封装使代码更加优雅 🍑

我们上面写的代码中,我们能否自己实现一个简易版本的创建线程(类似于C++11提供的线程库那样)的类呢?以及加锁和解锁能够使用RAII的思想来帮助我们完成呢?当然是可以的,我们可以自己实现一个更加优雅的代码:

mutexGuard.hpp:

#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;class mutexGurad
{
public:mutexGurad(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~mutexGurad(){pthread_mutex_unlock(_mutex);}private:pthread_mutex_t* _mutex;
};

thread.hpp:

#pragma once
#include <iostream>
#include <functional>
using namespace std;class threadProcess
{
public:enum stu{NEW,RUNNING,EXIT};template <class T>threadProcess(int num, T exe, void *args): _tid(0),_status(NEW),_exe(exe),_args(args){char name[26];snprintf(name, 26, "thread%d", num);_name = name;}static void *runHelper(void *args){threadProcess *ts = (threadProcess *)args; (*ts)();return nullptr;}void operator()() // 仿函数{if (_exe != nullptr)_exe(_args);}void Run(){int n = pthread_create(&_tid, nullptr, runHelper, this);if (n != 0)exit(-1);_status = RUNNING;}void Join(){int n = pthread_join(_tid, nullptr);if (n != 0)exit(-1);_status = EXIT;}private:string _name;pthread_t _tid;stu _status;function<void *(void *)> _exe;void *_args;
};

测试程序:

int g_tictet = 10000;
pthread_mutex_t mtu = PTHREAD_MUTEX_INITIALIZER;void *Run(void *args)
{string name = static_cast<const char *>(args);while (true){{mutexGurad mutGuard(&mtu);if (g_tictet <= 0){break;}else{cout << "I am " << name << ",is running  tickets" << g_tictet << endl;g_tictet--;}}usleep(1000);}return nullptr;
}int main()
{threadProcess thpro1(1, Run, (void *)"thread1");threadProcess thpro2(2, Run, (void *)"thread2");threadProcess thpro3(3, Run, (void *)"thread3");thpro1.Run();thpro2.Run();thpro3.Run();thpro1.Join();thpro2.Join();thpro3.Join();return 0;
}

当我们运行时:
在这里插入图片描述
我们依旧能够得到正确的结果,并且代码写起来也好看多了。除此之外,我们还可以拿到线程的其他特性,这里我就不在测试了。

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

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

相关文章

js 下载url返回的excel数据,并解析为json

XLSX GitHub地址&#xff1a;https://github.com/SheetJS/sheetjs/blob/github/dist/xlsx.full.min.js 需要先引入&#xff1a;XLSX.full.min.js // 下载文件的请求 fetch(downloadFileUrl).then(response > {return rsp.blob() }).then(data > {let reader new FileR…

【密码学】三、AES

AES 1、AES产生2、数学基础2.1有限域GF(2^8^)2.1.1加法运算2.1.2乘法运算2.1.3x乘运算2.1.4系数在GF(2^8^)上的多项式 3、AES算法描述3.1字节代换3.2行移位3.3列混合3.4轮密钥加3.5密钥扩展 1、AES产生 征集AES算法的活动&#xff0c;目的是确定一个非保密的、公开的、全球免费…

HCIP——重发布及路由策略实验

重发布及路由策略实验 一、实验拓扑二、实验要求三、实验思路三、实验步骤1、配置接口IP地址以及环回地址2、配置动态路由协议3、重发布4、更改接口类型5、配置路由策略 一、实验拓扑 二、实验要求 1、使用双点双向重发布2、所有路由器进行最佳选路3、存在备份路径&#xff0c…

软考05根据内存区域大小计算芯片数量

文章目录 前言一、原题二、解题思路1.计算内存区域的大小2.计算每个存储器芯片的容量3.计算芯片数量 总结 前言 从网上看题答案是有了&#xff0c;但是不知道具体的计算过程就很难受&#xff0c;不然下次还是不会&#xff0c;只能自己梳理了 一、原题 二、解题思路 1.计算内存…

Android开发之Fragment动态添加与管理

文章目录 主界面布局资源两个工具Fragment主程序 主界面布局资源 在activity_main.xml中&#xff0c;声明两个按钮备用&#xff0c;再加入一个帧布局&#xff0c;待会儿用来展示Fragment。 <?xml version"1.0" encoding"utf-8"?> <LinearLayo…

手机的python怎么运行文件,python在手机上怎么运行

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;手机上的python怎么运行程序&#xff0c;手机的python怎么运行文件&#xff0c;今天让我们一起来看看吧&#xff01; 1、python程序怎么在手机上运行 python语言应用很广泛&#xff0c;自己也很喜欢使用它&#xff0c;其…

iOS - 检测项目中无用类和无用图片

一、无引用图片检测 LSUnusedResources 安装插件 LSUnusedResources &#xff0c;用【My Mac】模拟器运行,如下图&#xff1a; Project Path 就是项目所在的路径&#xff0c;然后点击右下角 Search按钮&#xff0c;就可以看到被搜索出来的图片资源。 注意&#xff1a;这里被搜…

springboot 序列化相关问题汇总(持续更新...)

一、序列化问题 没有过多的性能需求&#xff0c;建议使用spring内置的jackjson&#xff0c;安全上更有保障 &#xff08;1&#xff09;返回数据 返回空字段 /*** 处理空字段* param builder* return*/Beanpublic ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilde…

Lambda 表达式的作用域

在Lambda表达式中访问外层作用域和旧版本的匿名对象中的方式类似。你可以直接访问标记了final的外层局部变量&#xff0c;或者实例的字段以及静态变量。 Lambda表达式不会从超类&#xff08;supertype&#xff09;中继承任何变量名&#xff0c;也不会引入一个新的作用域。Lambd…

路由器(第二十五课)

路由器的深入学习 一、路由 1、路由 1) 什么是路由:路由就是数据包从一个网络到另外一外网络的过程 2)支持路由功能的设备:路由器、三层交换机、防火墙 3 路由器转发数据包的依据: -每一台路由器都维护着一张路由表 -路由器是依靠这张路由表来转发数据的 -这张路由表就…

Linux——进程控制

目录 1. 进程创建 1.1 fork函数 1.2 fork系统调用内部宏观流程 1.3 fork后子进程执行位置分析 1.4 fork后共享代码分析 1.5 fork返回值 1.6 写时拷贝 1.7 fork常规用法 1.8 fork调用失败的原因 2.进程终止 2.1 进程退出场景 2.2 strerror函数—返回描述错误号的字符…

解决问题:python PermissionError: [WinError 5]拒绝访问

重要&#xff1a;关闭PyCharm Community Edition 2022.3等与python相关的编程程序找到按照python解释器的位置python->右键>属性>安全->点击组或用户名"中的Users->编辑点击"组或用户名"中的Users->把"完全控制"打钩->应用->…

《算法竞赛·快冲300题》每日一题:“最小生成树”

《算法竞赛快冲300题》将于2024年出版&#xff0c;是《算法竞赛》的辅助练习册。 所有题目放在自建的OJ New Online Judge。 用C/C、Java、Python三种语言给出代码&#xff0c;以中低档题为主&#xff0c;适合入门、进阶。 文章目录 题目描述题解C代码Java代码Python代码 “ 最…

09-特殊的向量

0 向量 就是分量全为 0 的向量 &#xff08;0 0 … 0&#xff09;单位向量 就是 L2 范数/模/长度为 1 的向量 如果一个向量大部分的位置为0&#xff0c; 少部分为非0的数&#xff0c; 那这样的向量我们就称之为稀疏向量&#xff0c; 反之为稠密向量&#xff0c; 它们是数学里面…

5、VMWARE安装、MobaXterm SSH连接 、Ubuntu xrdp安装使用

以下是在VMware中安装Ubuntu 22.04的详细步骤&#xff1a; 下载Ubuntu 22.04镜像文件&#xff1a; 前往Ubuntu官方网站或其他可信来源&#xff0c;下载Ubuntu 22.04的镜像文件&#xff08;.iso格式&#xff09;。 创建虚拟机&#xff1a; 打开VMware Workstation软件&#xf…

Servlet文件的下载

第一种方法直接在前端使用超链接&#xff0c;也就是a标签 浏览器不能识别会直接下载&#xff08;像压缩文件不能直接下载&#xff09;&#xff0c;浏览器能识别&#xff0c;想要下载加一个download属性。download可以不写任何信息。 首先在web下建一个文件&#xff0c;放需要…

在Windows 10和11中恢复已删除的照片

可以在Windows 10或11上恢复已删除的照片吗&#xff1f; 随着技术的发展&#xff0c;越来越多的用户习惯在电子设备上存储照片。如果这些照片被删除&#xff0c;可能会给用户带来重大损失。当照片丢失时&#xff0c;您可能会想是否可以恢复已删除的照片&#xff1f; …

Kafka原理剖析

一、简介 Kafka是一个分布式的、分区的、多副本的消息发布-订阅系统&#xff0c;它提供了类似于JMS的特性&#xff0c;但在设计上完全不同&#xff0c;它具有消息持久化、高吞吐、分布式、多客户端支持、实时等特性&#xff0c;适用于离线和在线的消息消费&#xff0c;如常规的…

内网隧道代理技术(十五)之 Earthworm的使用(二级代理)

Earthworm的使用(二级代理) 本文紧接着上一篇文章继续讲解Earthworm工具的使用 (二级代理)正向连接 二级正向代理发生在如下的情况: 1、Web服务器在公网,黑客可以直接访问 2、B机器在内网,黑客不能直接访问 3、Web服务器可以访问内网机器B 4、内网机器B可以访问公司…

ARM将常数加载到寄存器方法之LDR伪指令

一、是什么&#xff1f; LDR Rd,const伪指令可在单个指令中构造任何32位数字常数,使用伪指令可以生成超过MOV和MVN指令 允许范围的常数. 实现原理: (1)如果可以用MOV或MVN指令构造该常数,则汇编程序会生成适当的指令 (2)如果不能用MOV或MVN指令构造该常数,则汇编程序会执行下列…