多线程与线程互斥

目录

引言

一、多线程设计

多线程模拟抢票

二、互斥锁

互斥量的接口

修改抢票代码

锁的原理

锁的封装:RAII


引言

随着信息技术的飞速发展,计算机软件正变得越来越复杂,对性能和响应速度的要求也日益提高。在这样的背景下,多线程编程作为一种提高程序执行效率和资源利用率的技术,已经成为了现代软件开发中的重要组成部分。然而,多线程技术在带来性能提升的同时,也引入了一系列复杂的问题,如资源共享、数据一致性和线程间的协调。为了确保多线程环境下的程序正确性和稳定性,线程同步与线程互斥成为了我们必须深入探讨的关键领域。上文讲解了用户级线程(linux是用户级线程,win是内核级线程)本文将引导读者了解多线程编程的基本概念,探讨线程同步与线程互斥的重要性,并分析如何在多线程应用程序中有效地管理和控制线程行为。

一、多线程设计

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>using namespace std;class threadData
{
public:threadData(const string& name) :_threadname(name){}public:string _threadname;
};void* threadRountine(void* arg)
{pthread_detach(pthread_self()); //detach thread from main thread,但是不能保证主线程最后退出!threadData* td = static_cast<threadData*>(arg);int i = 0;while (i < 5){cout << td->_threadname << "    ,test_i = " << i << ", &test_i = " << (&i)<< endl;i++;    sleep(1);}delete td;return nullptr;
}int main()
{vector<pthread_t> tids;for (int i = 0; i < 5; i++){pthread_t tid;threadData* td = new threadData("Thread" + to_string(i));pthread_create(&tid, nullptr, threadRountine, td);tids.push_back(tid);sleep(1);}return 0;
}

可以观察到,栈内的数据是线程独享的。每个线程都有自己独立的栈结构.地址不同,变量的数值也不同,同时哪个线程先执行也是未知的。

需要注意的是,我们观察到有的线程并没有跑完5个完整的循环,这是因为:pthread_detach函数不能保证主线程最后退出,但是pthread_join函数能保证主线程最后退出,如果主线程退出,那么进程就会退出。

其实在线程当中没有秘密(都在一个地址空间)。我们可以定义一个全局的变量,让线程中使用这个变量,并实例化这个变量,我们就可以在主线程、其他线程获得这个数据。

虽然可以,但是线程独立栈上的数据不要透露出去!-----保证独立,但不能保证私有

即,如何让一个全局变量拥有线程私有的数值呢?----线程的局部存储技术

__thread:这是编译器支持的编译选项。只能定义内置类型,禁止自定义

发现地址并不一致。因此对g_val ++ 的时候,g_val属于线程私有。而且之前的地址很小,在全局区。现在的地址很大,就像是在堆栈一样大。

主函数:已初始化全局区 线程函数:共享区

用途:减少相似代码的多次编写

多线程模拟抢票

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <string>using namespace std;int tickets = 100;class threadData
{
public:threadData(int number) :_threadname("Thread" + to_string(number)){}public:string _threadname;
};void* GetTickets(void* arg)
{threadData* td = static_cast<threadData*>(arg);while (true){if (tickets > 0){usleep(10000); cout << td->_threadname << " got a ticket : " << tickets << endl;  tickets--;}elsebreak;}cout << td->_threadname << " : quit" << endl;return nullptr;
}int main()
{vector<pthread_t> tids;vector<threadData*> tds;for (int i = 0; i < 5; i++){pthread_t tid;threadData* td = new threadData(i);tds.push_back(td);pthread_create(&tid, nullptr, GetTickets, td);tids.push_back(tid);}for (int i = 0; i < tids.size(); i++){pthread_join(tids[i], nullptr); //原型:int pthread_join(pthread_t thread, void **retval);delete tds[i];} return 0;
}

我们设置了100张tickets,让5个线程去抢票。

发现抢票的逻辑出错了。

原因:

1.显示器是一个共享资源,没有得到保护,各个线程抢占显示器资源,导致打印出错。

2.tickets是一个共享资源,多个线程并发访问的时候,会导致临界资源收不到保护。作为临界资源,必须保证一次只能被一个执行流进入访问。并发访问是指多个执行单元(如线程、进程或任务)在同一时间段内试图同时访问共享资源(如内存、文件、数据库等)的情况

为什么可能无法获得争取结果?
        if 语句判断条件为真以后,代码可以并发的切换到其他线程        
        usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
        --ticket 操作本身就不是一个原子操作

--操作需要三部才能完成

--操作不是原子的,不是安全的

二、互斥锁

互斥量的接口

初始化互斥量
初始化互斥量有两种方法:
方法 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_t这是库给我们提供的一种数据类型

可以定义成全局,用这个宏初始化

定义成全局之后,不需要destroy销毁

不是全局,就需要借助函数初始化和销毁

初始化函数的第二个参数是属性相关,可以不用管,设置为nullptr

并发访问需要保证所有线程申请的是同一把锁。

销毁互斥量
销毁互斥量需要注意
使用 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_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

加锁和解锁之间的区域,就可以保证数据是安全的访问的

为了让tickets数据是安全的,在访问tickets数据的阶段就需要加锁。

我们成功加锁之后,这个tickets全局变量就变成了临界资源。

在我们的代码中,只有一小部分在访问临界资源,这一部分叫做临界区

互斥:在任何时刻,只让一个执行流去访问临界资源。

锁:拒绝并发,串行执行,保护数据的安全。

锁之间的代码是原子性的

/*

并发(Concurrency)

并发是指系统能够处理多个任务同时存在的能力。在并发的情况下,任务可以交替进行,即一个任务开始执行,在它执行的过程中,可以暂停并让另一个任务执行,然后第一个任务可以在适当的时候恢复执行。并发强调的是任务管理的灵活性,而不是速度。

并发的一些关键点包括:

时间共享:多个任务共享同一个处理器的执行时间。

上下文切换:操作系统可以在任务之间快速切换,某个时间段同时处理多个任务。

协调:并发系统需要处理任务之间的资源共享和通信问题。

并行(Parallelism)

并行是指同时执行多个任务的能力。并行执行通常需要多个处理器或多核处理器,这样每个处理器可以同时处理一个任务或任务的一部分(多个cpu,可以一次从执行队列调出多个线程去执行)。

并行的一些关键点包括:

同时性:多个任务或多个部分同时在不同的处理器上执行。

硬件依赖:并行执行通常需要特定的硬件支持,如多核CPU或分布式系统。

*/

修改抢票代码

增加上锁保护机制。

class threadData
{
public:threadData(int number, pthread_mutex_t* mutex) :_threadname("Thread" + to_string(number)),_mutex(mutex){}public:string _threadname;pthread_mutex_t* _mutex;
};

需要在任何形式的访问共享资源前加锁。抢完一次票需要解锁。

解锁需要在break之前,防止break之后直接跳出循环,这样最后一次循环中申请的锁就无法解锁。

现象

发现票都被两个线程抢完了,虽然不会出现tickets出错,但是出现了逻辑出错。

为什么不把锁放在while外部呢?---当然不可以

逻辑上:1.不可以把票一次性--完 2.增加了执行负担,降低并发效率

执行临界区的代码需要先申请锁,这个锁只有一把(所有线程只能看到同一把锁)。

申请锁成功才能去执行临界资源,否则执行流会在锁处阻塞

申请锁失败:1.正常申请失败 2.锁被其他人拿走……这个时候线程会等待锁资源。

虽然票不是负数,但是被一个线程抢完了。

加上休眠时间

只要不访问临界区了,就可以解锁。

这样就可以保证规则地去抢锁。

/*

锁与时间片:当一个线程获得锁并开始执行被加锁的代码块时,它可能会占用一个或多个时间片。如果在执行过程中时间片用尽,操作系统可能会进行线程调度,将该线程挂起,并将CPU时间分配给其他线程。但是,即使发生线程切换,持有锁的线程在释放锁之前,其他线程是无法进入该锁保护的代码块的。

也就说,就算锁资源在执行的过程中因为时间消耗完毕而被切换,那么其他执行流在执行的时候(线程是被执行调度的基本单位),也禁止访问“被锁住”的资源,从而保证资源的安全性,从而在现象上达成了原子性的现象

*/

全局锁:不需要初始化与销毁

(用全局的宏初始化)内部加锁就直接用全局的锁就可以。

即:省去了init与destory函数

但是还是得手动lock与unlock。

这个时候就不需要类内封装锁,使用全局的锁就可以

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

之前代码的错误:

锁被释放之后,那么在循环又接着去申请锁,导致不断地申请释放锁,从而导致资源一直被锁在一个执行流(线程)之中。

大白话就是:你这个线程离这个锁更近,每个线程竞争锁的能力是不同的。

当修改之后,这个进程在休眠期间,其他进程就有机会去竞争锁资源。

我们对于锁的申请是允许并发(一段时间内,各个执行流都有机会被执行)的,只不过上锁(lock)这句代码是原子的。

条件变量:线程同步解决饥饿问题

锁的原理

锁也是共享的。我们对于锁的申请是允许并发(一段时间内,各个执行流都有机会被执行)的,只不过上锁(lock)这句代码是原子的。

但是

该部分将着重解决两个问题,第一个问题就是锁的原理问题。

问题2

答案是可以切换的

这是锁通过对线程的控制完成的。

问题一解答:锁的原理

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我
们把lock和unlock的伪代码改一下
上锁:
1.先把al寄存器置0
2.再把al和mutex的数据做交换。
如果al寄存器中的数据是1--得到了锁,那么就会申请锁结束
如果al寄存器中的数据是0,代码就阻塞在这个地方,持续申请锁,知道锁的到来。
解锁:
锁被申请到之后,mutex内部就存储的是0。解锁只需要让mutex重新存储1即可,这样其他线程就能再次申请锁。

唤醒其他线程,让锁的内容从0变1(上锁经历了xchg之后,锁的内容为0),这样就恢复到了锁初始化的状态,让其他线程有能力申请锁。

锁的封装:RAII

#pragma once#include <pthread.h>class Mutex 
{
public:Mutex(pthread_mutex_t* lock): _lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void Unlock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t* _lock; 
};class LockGuard     // RAII class for locking and unlocking a mutex---需要传入指针
{
public:LockGuard(pthread_mutex_t* mutex): _mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex _mutex;
};

在使用的时候,我们应该在外面建立一个锁的指针,就像是智能指针一样。

注意:

1,限定作用域,出了作用域之后,这个临时对象会自动调用析构去解锁

2,业务的处理时间不应该在if内,不应该在加锁内部。

发现正确抢完了票。

由于打印在锁的内部,所以显示器资源也被保护了。

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

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

相关文章

ZUC256 Go Go Go!!!

文章目录 背景运行效果代码 背景 因业务需要使用ZUC算法&#xff0c;GitHub上又没有对ZUC256相对应的Go语言的实现。 吃水不忘挖井人&#xff0c;在这里感谢GmSSL及BouncyCastle两个强大的密码学库&#xff01; 本ZUC256的编写&#xff0c;参考了这两个库及中科院软件院发布的…

力扣打卡12:复原IP地址

链接&#xff1a;93. 复原 IP 地址 - 力扣&#xff08;LeetCode&#xff09; 这道题需要对字符串进行操作&#xff0c;我选择了三层循环&#xff0c;其实还可以递归。 我在循环时进行了剪枝&#xff0c;比如一些情况直接跳出循环。 我的代码&#xff1a; class Solution { p…

【实践·专业课】内存管理-存储管理-文件系统

1. 基于Linux的简单区块链实现 1.1. 环境准备 确保使用的 Linux 系统&#xff08;如 Ubuntu、CentOS 等&#xff09;已安装 Python 3。 在终端输入python3命令&#xff0c;若出现 Python 解释器的版本信息等提示&#xff0c;则表示已安装&#xff1b; 若提示未找到命令&…

2个GitHub上最近比较火的Java开源项目

1. SpringBlade 微服务架构 标题 SpringBlade 微服务架构 摘要 SpringBlade 是一个由商业级项目升级优化而来的微服务架构&#xff0c;采用Spring Boot 3.2、Spring Cloud 2023等核心技术构建&#xff0c;遵循阿里巴巴编码规范&#xff0c;提供基于React和Vue的两个前端框架&am…

MongoDB 建模调优change stream实战

MongoDB开发规范 &#xff08;1&#xff09;命名原则。数据库、集合命名需要简单易懂&#xff0c;数据库名使用小写字符&#xff0c;集合名称使用统一命名风格&#xff0c;可以统一大小写或使用驼峰式命名。数据库名和集合名称均不能超过64个字符。 &#xff08;2&#xff09…

Ubuntu 环境美化

一、终端选择 zsh 参考文章使用 oh-my-zsh 美化终端 Oh My Zsh 是基于 zsh 命令行的一个扩展工具集&#xff0c;提供了丰富的扩展功能。 先安装zsh再安装Oh My Zsh 1.zsh安装 sudo apt-get install zsh 2.设置默认终端为 zsh chsh -s /bin/zsh 3.安装 oh-my-zsh 官网&…

分布式事务的前世今生-纯理论

一个可用的复杂的系统总是从可用的简单系统进化而来。反过来这句话也正确: 从零开始设计的复杂的系统从来都用不了&#xff0c;也没办法让它变的可用。 --John Gal 《系统学》 1975 1. 事务的概念 百科&#xff1a; 事务&#xff08;Transaction&#xff09;&#xff0c;一般是…

操作系统的文件系统

文件系统的基本组成 ⽂件系统是操作系统中负责管理持久数据的⼦系统&#xff0c;说简单点&#xff0c;就是负责把⽤户的⽂件存到磁盘硬件中&#xff0c; 因为即使计算机断电了&#xff0c;磁盘⾥的数据并不会丢失&#xff0c;所以可以持久化的保存⽂件。 ⽂件系统的基本数据单位…

vue使用百度富文本编辑器

1、安装 npm add vue-ueditor-wrap 或者 pnpm add vue-ueditor-wrap 进行安装 2、下载UEditor 官网&#xff1a;ueditor:rich text 富文本编辑器 - GitCode 整理好的&#xff1a;vue-ueditor: 百度编辑器JSP版 因为官方的我没用来&#xff0c;所以我自己找的另外的包…

浅谈自然语言处理技术(NLP)在银行领域的应用

自然语言处理技术(NLP)通过解析和理解海量非结构化数据,为银行领域提供了前所未有的洞察力和决策支持。这项技术的应用不仅优化了风险管理,还革新了客户服务和市场分析等多个方面。 银行系统中存在大量的非结构化信息,这些信息不仅数据量庞大,而且种类繁多,处理起来相对…

nvm安装指定版本显示不存在及nvm ls-remote 列表只出现 iojs 而没有 node.js 解决办法

在使用 nvm install 18.20.3 安装 node 时会发现一直显示不存在此版本 Version 18.20.3 not found - try nvm ls-remote to browse available versions.使用 nvm ls-remote 查看可安装列表时发现&#xff0c;列表中只有 iojs 解决方法&#xff1a; 可以使用以下命令查看可安装…

Linux磁盘存储概念(六)

本文为Ubuntu Linux操作系统- 第六弹 今天开始新的知识点&#xff0c;讲磁盘存储问题 上期回顾&#xff1a;Linux文件、目录权限问题 今天由帝皇侠陪伴大家学习&#xff01;&#xff01;&#xff01; 文章目录 磁盘数据组织低级格式化磁盘分区高级格式化 磁盘设备命名磁盘分区分…

鼠标右键单击Git Bash here不可用

最近在学习git时突然发现右键的git bash没反应&#xff0c;但是去点击应用图标就能正常运行&#xff0c;通常是因为你在安装git之后改变了它的目录名称或者位置&#xff0c;我就是因为安装后改变了一个文件夹的文件名导致不可用 在安装git时系统会默认给鼠标右键选项的git Bas…

计算机网络之NAT、代理服务、内网穿透、内网打洞

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 计算机网络之NAT、代理服务、内网穿透、内网打洞 收录于专栏【计算机网络】 本专栏旨在分享学习计算机网络的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论…

大数据新视界 -- 大数据大厂之 Hive 数据导入:多源数据集成的策略与实战(上)(3/ 30)

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

光猫开DMZ教程

本教程以移动光猫未例&#xff0c;具体操作以实际光猫为准 1、登录移动光猫管理后台 打开浏览器&#xff0c;在浏览器地址栏输入移动光猫登录管理地址192.168.1.1或者tplogin.cn 按“回车键”打开登录页面&#xff0c;然后输入路由器管理密码登录。 移动光猫登录页面 超级密…

分数求和ᅟᅠ        ‌‍‎‏

分数求和 C语言代码C 代码Java代码Python代码 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 输入n个分数并对他们求和&#xff0c;并用最简形式表示。所谓最简形式是指&#xff1a;分子分母的最大公约数为1&#xff1b;若最终结果的分母为…

【经典论文阅读】Latent Diffusion Models(LDM)

Latent Diffusion Models High-Resolution Image Synthesis with Latent Diffusion Models 摘要 动机&#xff1a;在有限的计算资源下进行扩散模型训练&#xff0c;同时保持质量和灵活性 引入跨注意力层&#xff0c;以卷积方式实现对一般条件输入&#xff08;如文本或边界框…

交换瓶子(图论 贪心)

1224. 交换瓶子 - AcWing题库 把每一个瓶子看成一个点&#xff0c;从每个瓶子向他应该在的那个位置的瓶子连一条边 通过这个方式&#xff0c;我们就可以连出n条边 观察可以发现这些图有特点&#xff1a; n个点 连成n条边 因为每个点会指向它应该在的位置的那个点&#xff…

汽车免拆案例 | 2007款宝马650i车发动机偶尔无法起动

故障现象 一辆2007款宝马650i车&#xff0c;搭载N62B48B发动机&#xff0c;累计行驶里程约为26万km。车主反映&#xff0c;发动机偶尔无法起动&#xff0c;故障频率较低&#xff0c;十几天出现1 次&#xff0c;且故障出现时起动机不工作。 故障诊断  接车后试车&#xff0c;…