38 线程互斥

目录

1.问题
2.互斥相关概念
3.互斥量
4.互斥量接口
5.修改买票代码
6.互斥量原理
7.锁的封装
8.可重入和线程安全

1. 问题

用一个模拟抢票过程的程序引出本节话题,如果有1000张票,设为全局变量,生成3个线程每隔一秒抢一张票,如果票数大于0就继续,小于0就退出,看看结果:

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>
#include <errno.h>
#include <cstring>using namespace std;
int ticket = 1000; //票数量
void *buyticket(void *num)
{int n = (long)num;while (true){if (ticket > 0){usleep(1000);ticket--;printf("%d 线程买到票,剩余 %d 张\n", n, ticket);}else{break;}}
}int main()
{vector<pthread_t> v;for (int i = 0; i < 3; i++){pthread_t tid;pthread_create(&tid, nullptr, buyticket, (void*)(long)i);v.push_back(tid);}for(auto ch : v){pthread_join(ch, nullptr);}return 0;
}

在这里插入图片描述

票没有了,剩余负数张,这是有问题的

原因

因为ticket是全局变量,属于共享资源,每个线程都可以访问。当if判断和票数减减时有可能一个线程刚判断票数还有,准备往下执行或–时,其他的线程也来判断,这时,当票数只剩1张时,三个线程都会判断为有票,执行到下面–3次,票数就成了负数

这就是多线程访问共享数据引起的数据不一致问题,线程会在内核返回用户的时候检查切换

–是否安全

对一个全局变量进行多线程并发的–或者++是不是安全的?
cpu在对一个数据减减时需要三步操作,所以是不安全的
在这里插入图片描述

1.先将ticket读入到寄存器中
2.cpu内部计算
3.将计算结果写回内存
上面每一步都对应一条汇编操作,只有一条汇编指令才是原子的,上面的三步操作中都有可能切换线程。线程1先读入数据。if判断属于逻辑运算,需要读入数据,进行判断,跳转执行,也不是原子的。如果票数只剩1张,几条线程都判断大于1进入内部,当1条线程–后将数据改为0写回内存,其他线程进来执行,别忘了,–操作需要重新从内存中读入数据,所以会在0的基础上继续–,就变成了-1

怎么解决
对共享收据的任何访问,保证在任何时候只有一个执行流访问,就是互斥

2. 互斥相关概念

临界资源:多线程执行流共享的资源叫临界资源
临界区:每个线程内部,访问临界资源的代码,叫临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

3. 互斥量

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互
多个线程并发的操作共享变量,会带来一些问题
上面的抢票由下面三个地方可能被切换:
1.if语句判断为真以后,代码可以并发的切换到其他线程
2.usleep这个模拟漫长业务的过程,可能会有多个线程进入该代码片段
3.–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上提供的这把锁叫互斥量
在这里插入图片描述

4. 互斥量接口

初始化互斥量

初始化互斥量由两种方法:
方法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

销毁互斥量

需要注意:
使用PTHREAD_MUTEX_INTIALIZER初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁

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_lock时,可能会遇到以下情况:

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

加锁总结

加锁的本质:用时间换取安全
加锁的表现:线程对于临界区代码串行执行
加锁原则:尽量的保证临界区代码越少越好,串行执行的代码就越少,其他线程等的时间就越少

5. 修改买票代码

局部变量锁

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>
#include <errno.h>
#include <cstring>using namespace std;
int ticket = 1000; //票数量struct threadData
{threadData(int num, pthread_mutex_t *_lock){i = num;lock = _lock;}int i;pthread_mutex_t *lock;
};
void *buyticket(void *num)
{threadData* td = static_cast<threadData*>(num);while (true){//加锁pthread_mutex_lock(td->lock);if (ticket > 0){usleep(1000);ticket--;printf("%d 线程买到票,剩余 %d 张\n", td->i, ticket);//解锁pthread_mutex_unlock(td->lock);}else{pthread_mutex_unlock(td->lock);break;}usleep(13);  //买完票不会去买下一张,延时模拟过程}
}int main()
{vector<pthread_t> v;vector<threadData*> td;// 初始化互斥量pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr);for (int i = 0; i < 3; i++){td.push_back(new threadData(i, &lock));pthread_t tid;pthread_create(&tid, nullptr, buyticket, td[i]);v.push_back(tid);}for(auto ch : v){pthread_join(ch, nullptr);}return 0;
}

在这里插入图片描述

这次运行正常了,买票之后加上usleep,省略买成功后的动作,如果没有,会造成加锁的线程刚买完又立马继续加锁,导致其他线程难以得到锁的使用权,无法进入判断。线程对锁的竞争能力是不同的

纯互斥环境,如果锁分配不合理,容易导致其他线程的饥饿问题。不是说只要有互斥,必有饥饿。适合纯互斥的场景,就用互斥
让所有的线程获取锁,按照一定顺序获取资源,就是同步

线程申请锁成功,才能向后执行,不成功则阻塞等待。在临界区,线程可以切换,但线程切换出去是持有锁被切走,不在期间,照样没有人能进入临界区访问资源。对于其他线程来讲,一个线程要么没有锁,要么释放锁,当前线程访问临界区的过程,对于其他线程都是原子的。不释放锁,其他线程一直申请不到锁,就阻塞着

全局变量锁

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <cstring>
#include <thread>
#include <vector>
#include <errno.h>
#include <cstring>using namespace std;
int ticket = 1000; //票数量
//全局变量锁,自动解锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;struct threadData
{threadData(int num/*, pthread_mutex_t *_lock*/){i = num;//lock = _lock;}int i;//pthread_mutex_t *lock;
};
void *buyticket(void *num)
{threadData* td = static_cast<threadData*>(num);while (true){//加锁pthread_mutex_lock(&lock);if (ticket > 0){usleep(1000);ticket--;printf("%d 线程买到票,剩余 %d 张\n", td->i, ticket);//解锁pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break;}usleep(13);  //买完票不会去买下一张,延时模拟过程}
}int main()
{vector<pthread_t> v;vector<threadData*> td;// 初始化互斥量pthread_mutex_t lock;//pthread_mutex_init(&lock, nullptr);for (int i = 0; i < 3; i++){td.push_back(new threadData(i/*, &lock*/));pthread_t tid;pthread_create(&tid, nullptr, buyticket, td[i]);v.push_back(tid);}for(auto ch : v){pthread_join(ch, nullptr);}for(auto ch : td){delete ch;}//pthread_mutex_destroy(&lock);return 0;
}

全局变量不需要初始化和销毁

6. 互斥量原理

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

在这里插入图片描述
在这里插入图片描述
将互斥量简单看做上述样子。初始化互斥量就会在内存里申请一个整数1,每一个线程上锁的过程就是上面的代码,al是eax寄存器的低16位,将这个寄存器值设为0,线程进来后寄存器和互斥量1进行交换,寄存器中变为1,把共享的锁以汇编的方式交换到了自己的上下文。内存中变为0,判断寄存器里值大于0就返回上锁成功。这时其他线程进来执行时,寄存器和内存中都为0,判断后就不满足条件而挂起等待。所谓的“锁”只有一个,只能被一个线程拥有,保证了加锁过程的原子性

在这里插入图片描述

解锁的过程将内存中互斥值改为1,然后唤醒等待的线程就可以像上面一样继续有一个线程得到锁。这里为什么不也用交换的方式,而是直接赋值?这样可以让一个线程上锁,另一个线程可以解锁,对解锁的一方没有要求。如果上锁的线程卡死了,不解锁的话其他线程也无法执行,所以可以解决这种情况

7. 锁的封装

一个锁类,有加锁和去锁的功能,再用一个类封装这个锁类成员,构造自动上锁,析构去锁。修改买票功能,线程判断票数的时候上锁,–完后去锁,避免延迟模式的功能到临界区,可以加一个域括号

锁类

#pragma once
#include <pthread.h>class Mutex
{
public:Mutex(pthread_mutex_t *_lock){m_lock = _lock;}void Lock(){pthread_mutex_lock(m_lock);}void unLock(){pthread_mutex_unlock(m_lock);}private:pthread_mutex_t *m_lock;
};class LockGuard
{
public:LockGuard(pthread_mutex_t *_lock):m(_lock){m.Lock();}~LockGuard(){m.unLock();}private:Mutex m;
};

买票

#include <stdio.h>
#include <unistd.h>
#include <cstring>
#include <thread>
#include <vector>
#include <errno.h>
#include <cstring>
#include "LockGuard.hpp"using namespace std;
int ticket = 1000; //票数量
//全局变量锁,自动解锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;struct threadData
{threadData(int num/*, pthread_mutex_t *_lock*/){i = num;//lock = _lock;}int i;//pthread_mutex_t *lock;
};
void *buyticket(void *num)
{threadData* td = static_cast<threadData*>(num);while (true){//加锁//pthread_mutex_lock(&lock);{LockGuard mutex(&lock);if (ticket > 0){usleep(1000);ticket--;printf("%d 线程买到票,剩余 %d 张\n", td->i, ticket);// 解锁// pthread_mutex_unlock(&lock);}else{// pthread_mutex_unlock(&lock);break;}}usleep(13);  //买完票不会去买下一张,延时模拟过程}
}int main()
{vector<pthread_t> v;vector<threadData*> td;// 初始化互斥量//pthread_mutex_t lock;//pthread_mutex_init(&lock, nullptr);for (int i = 0; i < 3; i++){td.push_back(new threadData(i/*, &lock*/));pthread_t tid;pthread_create(&tid, nullptr, buyticket, td[i]);v.push_back(tid);}for(auto ch : v){pthread_join(ch, nullptr);}for(auto ch : td){delete ch;}//pthread_mutex_destroy(&lock);return 0;
}

8. 可重入和线程安全

概念

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

常见的线程不安全的情况

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

常见的线程安全情况

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

常见的不可重入

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

常见可重入

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

可重入和线程安全联系

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

可重入和线程安全的区别

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

线程安全描述的是并发的情况,重入描述的是函数的特性。线程安全不一定是可重入的,可重入一定是线程安全的

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

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

相关文章

快速搭建 Web自动化测试框架

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片 &#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 在程序员的世界中&#xff0c;一切重复性的工作&#xff0c;都应该通过程序自动执行。「自动化测…

美国站群服务器常见的操作系统选项

美国站群服务器常见的操作系统选项 美国站群服务器是一种灵活且可扩展的服务器解决方案&#xff0c;可以用于托管和管理多个网站和应用程序。在选择合适的美国站群服务器时&#xff0c;一个重要的考虑因素是其支持的操作系统。本文将介绍美国站群服务器常见的操作系统选项&…

第十五届蓝桥杯省赛第二场C/C++B组H题【质数变革】题解

解题思路 首先&#xff0c;我们考虑一下整个数组都是由质数构成的情况。 当我们要将质数 x x x 向后移 k k k 个时&#xff0c;如果我们可以知道质数 x x x 在质数数组的下标 j j j&#xff0c;那么就可以通过 p r i m e s [ j k ] primes[j k] primes[jk] 来获取向后…

C语言实验-函数与模块化程序设计

一&#xff1a; 编写函数fun&#xff0c;其功能是&#xff1a;输入一个正整数&#xff0c;将其每一位上为偶数的数取出重新构成一个新数并输出。主函数负责输入输出&#xff0c;如输入87653142&#xff0c;则输出8642。&#xff08;main函数->fun函数&#xff09; #define _…

.NET 解决Linux下Couldn‘t find a valid ICU package installed on the system异常的问题

最近使用.NET发布独立的Linux x64程序&#xff0c;在CentOS7.9的最小化安装系统上运行&#xff0c;结果运行不了&#xff0c;启动就报异常“Process terminated. Couldnt find a valid ICU package installed on the system. Please install libicu (or icu-libs) using your p…

周三多《管理学原理》第3版/考研真题/章节练习题

普通高等教育“十一五”国家级规划教材《管理学原理》&#xff08;第3版&#xff0c;周三多、陈传明、龙静编著&#xff0c;南京大学出版社&#xff09;是我国高校广泛采用的管理学权威教材之一&#xff0c;也被众多高校&#xff08;包括科研机构&#xff09;指定为考研考博专业…

深度解析 Spring 源码:从BeanDefinition源码探索Bean的本质

文章目录 一、BeanDefinition 的概述1.1 BeanDefinition 的定位1.2 BeanDefition 的作用 二、BeanDefinition 源码解读2.1 BeanDefinition 接口的主要方法2.2 BeanDefinition 的实现类2.2.1 实现类的区别2.2.2 setBeanClassName()2.2.3 getDependsOn()2.2.4 setScope() 2.3 Bea…

关于修改hosts,浏览器并没有刷新生效的问题.

1.windows系统用cmd命令: ipconfig /flushdns 进行刷新.并查看本地解析是否已经刷新. 2.检查是否开了,代理,代理还是有影响的,关闭,不然不会生效 3.针对谷歌浏览器解决方案: 访问: chrome://net-internals/?#sockets 点击close idle sockets和flush socket pools,,,清…

2024年申请海外访问学者,需要注意些什么呢?

越来越多的国内医院对于医生职称晋升有海外经历要求&#xff0c;医生出于临床技能提高和科研思维拓展&#xff0c;自己也有海外进修的需求&#xff0c;并且可以了解世界先进医疗理念及科研进展&#xff0c;何乐而不为? 其实&#xff0c;很多医生已经赶上早班车&#xff0c;通…

AI大模型探索之路-实战篇2:基于CVP架构-企业级知识库实战落地

目录 前言 一、概述 二、本地知识库需求分析 1. 知识库场景分析 2. 知识库应用特点 3. 知识库核心功能 三、本地知识库架构设计 1. RAG架构分析 2. 大模型方案选型 3. 应用技术架构选型 4. 向量数据库选型 5. 模型选型 三、本地知识库RAG评估 四、本地知识库代码落地 1. 文件…

【QGIS如何进行WKT可视化】

点击插件——python控制台 渲染线图层wkt = LINESTRINGZ (405223.79236294992733746767 3294798.52338150842115283012 0, 405250.43677213048795238137 3294767.52838639216497540474 0, 405272.27118375105783343315 3294735.17657975200563669205 0, 405363.435978673573117…

局域网屏幕桌面监控哪个软件比较好?哪款好用?

在企业、教育机构或其他组织中&#xff0c;出于提高工作效率、保障数据安全、规范员工行为等目的&#xff0c;对局域网内电脑屏幕进行实时监控的需求日益凸显。 面对市场上众多屏幕监控软件&#xff0c;选择一款功能全面、稳定可靠且符合法规要求的产品至关重要。 在局域网屏幕…

【WEB前端2024】开源智体世界:乔布斯3D纪念馆-第15课-xcard方式跳转平行3D馆

【WEB前端2024】开源智体世界&#xff1a;乔布斯3D纪念馆-第15课-xcard方式跳转平行3D馆 使用dtns.network德塔世界&#xff08;开源的智体世界引擎&#xff09;&#xff0c;策划和设计《乔布斯超大型的开源3D纪念馆》的系列教程。dtns.network是一款主要由JavaScript编写的智…

【C/C++】动态内存管理(C:malloc,realloc,calloc,free || C++:new,delete)

&#x1f525;个人主页&#xff1a; Forcible Bug Maker &#x1f525;专栏&#xff1a; C | | C语言 目录 前言C/C内存分布C语言中的动态内存管理&#xff1a;malloc/realloc/realloc/freemallocrealloccallocfree C中的动态内存管理&#xff1a;new/deletenew和delete操作内…

港口数据复杂不知道如何监控?来试试FineVis所展现的智慧港口看板

一、智慧港口是什么 智慧港口代表着港口建设的未来趋势和发展方向。以信息物理系统为框架&#xff0c;智慧港口通过创新应用高新技术&#xff0c;实现了物流供给方和需求方之间的沟通&#xff0c;并将它们融入集疏运一体化系统中。这种系统极大地提升了港口及其相关物流园区对…

【LeetCode:1103. 分糖果 II + 模拟】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

LiveCD镜像文件的定制化

最近想要定制化一款属于自己的LiveCD镜像文件&#xff0c;并且里边封装好所需要的软件程序&#xff0c;本文将会记录具体的操作步骤&#xff0c;供有需要的人参考借鉴。 环境说明&#xff1a; 环境配置说明配置参数编码环境Centos7.9LiveCD文件CentOS-7-livecd-x86_64.iso 附…

西门子PCU50.3数控面板维修6FC5220-0AA31-2AA0

西门子数控面板维修&#xff0c;西门子工控机触摸屏维修6FC5247-0AA00-0AA3 西门子数控机床维修包括&#xff1a;840C/CE、840Di/DSL、840Di SL、802C S、802D SL、810D/DE、820D SL、S120数控电路板、数控伺服驱动模块、控制模块修、电源模块&#xff0c;西门子数控机床控制面…

Redis缓存介绍以及常见缓存问题:穿透、雪崩和击穿

概念 缓存就是数据交换的缓冲区&#xff08;Cache&#xff09;&#xff0c;是存贮数据的临时地方&#xff0c;一般读写性能较高。 作用&#xff1a; 降低后端负载 提高读写效率&#xff0c;降低相应时间 成本&#xff1a; 数据一致性成本 代码维护成本 运维成本 缓存更…

QT爱发函,介绍一下平替QT的八大桌面开发框架。

Qt是一款跨平台的C应用程序开发框架&#xff0c;它提供了丰富的库和工具&#xff0c;可以用于开发图形用户界面、嵌入式系统、移动应用等。Qt拥有商业版和开源版两种许可证&#xff0c;商业版需要支付授权费用&#xff0c;而开源版则可以免费使用。 对于替代Qt的框架&#xff0…