Linux线程互斥

1.用线程封装代码测试通过现象引出线程互斥

1.1代码测试

Thread.hpp

#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<pthread.h>
template<class T>
using func_t = std::function<void(T)>;template<class T>
class Thread
{
public:Thread(const std::string & threadname,func_t<T> func,T data):_tid(0),_isrunning(false),_threadname(threadname),_func(func),_data(data){}static void* ThreadRoutine(void * args){Thread* ts =static_cast<Thread*>(args);ts->_func(ts->_data);return nullptr;}bool Start(){int n = pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n==0){_isrunning = true;return true;}return false;}bool Join(){if(!_isrunning) return true;int n = pthread_join(_tid,nullptr);if(n==0){_isrunning = false;return true;}return false;}   std::string ThreadName(){return _threadname;}bool IsRunning(){return _isrunning;}~Thread(){}private:pthread_t _tid;bool _isrunning;std::string _threadname;func_t<T> _func;T _data;
};

main.cc

#include<iostream>
#include<string> 
#include<unistd.h>
#include"Thread.hpp"std::string GetThreadName()
{static int num = 1;char name[64];snprintf(name,sizeof(name),"Thread-%d",num++);return name;
}void Print(int num)
{while(num){std::cout<<"hello world: "<<num--<<std::endl;sleep(1);}
}int ticket = 10000;//全局的共享资源void GetTicket(std::string name)
{while(true){if(ticket>0){usleep(1000);printf("%s get a ticket: %d\n",name.c_str(),ticket);ticket--;}else {break;}}
}int main()
{std::string name1 = GetThreadName();Thread<std::string>t1(name1,GetTicket,name1);std::string name2 = GetThreadName();Thread<std::string>t2(name2,GetTicket,name2);std::string name3 = GetThreadName();Thread<std::string>t3(name3,GetTicket,name3);std::string name4 = GetThreadName();Thread<std::string>t4(name4,GetTicket,name4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();return 0;
}

运行结果:

1.2概念引入

在这次测试当中我们发现票数出现了负数,然而当我们真正现实生活中电影院抢票或者高铁飞机抢票有多少个位置它就卖多少张票,而不可能多卖票,否则座位会产生冲突,所以这里票数出现了负数说明多卖出去了几张票,所以票数卖到负数是不合理的。我们把这种情况叫做数据不一致,所以对于这部分共享资源为了防止出现数据不一致的情况,我们是要把这部分共享资源保护起来的,怎么保护呢?我们只需要保证任何一个时刻只允许一个线程正在访问共享资源。被保护起来的资源我们叫做临界资源。我们把访问临界资源的代码叫做临界区

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

1.3解释原因

下面我们看一段简单的反汇编代码:

其实一条简单的a++指令是需要三条汇编语句的。其实第一步就是先从内存中将a拷贝到CPU的寄存器当中然后第二步是通过计算对a进行++,第三步是把CPU寄存器a++后的值拷贝回内存中的。

而一般情况是将一条汇编语句看作原子性。多线程并发访问全部int,不是原子的会有数据不一致的并发访问问题。

而同理

这段代码也同上述一样,ticket--;也是通过执行三条汇编语句,不是原子的。而判断本身也是计算,也就是if(ticket>0)这条语句。而CPU大概总结下来分四种功能:

算术运算,逻辑运算,处理内外中断,控制单元。

通过上述的说法,那么就会有这样一种情况,当我们的四个线程都在通过if判断之后进入到临界区,此时ticket=1;那么在时间片到之后进行了切换,然后四个线程依次执行ticket--操作,所以就会导致ticket=1时被4个线程减4次,导致减到了-2这种情况。因为数据在内存中,本质是被线程共享的,数据被读取到寄存器中,本质变成了线程的上下文,属于线程私有数据。

而如何对这种共享资源进行保护呢?通过加锁

2.理解锁

2.1认识接口

初始化互斥量
初始化互斥量有两种方法:

方法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_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

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

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

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

对临界区进行加锁:

1.我们要尽可能的给少的代码块加锁

2.一般加锁都是给临界区加锁

这里的锁也是全局变量,所以也是共享资源,而申请锁本身安全吗?申请锁本身是安全的,因为他是原子的。

运行结果:

我们发现执行速度变慢了,但是没有出现负数的情况了。

根据互斥的定义,任何时刻只允许一个线程申请锁成功,多个线程申请锁失败,失败的线程怎么办?在mutex上进行阻塞,本质就是等待。

一个线程在临界区中访问临界资源的时候是可能会发生切换的。

除了把锁定义为全局的,还可以将锁定义成局部的,定义局部的锁需要用到这个pthread_mutex_init这个接口来进行初始化,用phtread_mutex_destroy来销毁:

2.2用锁解决问题

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

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

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

将代码进行部分修改:

main.cc#include<iostream>
#include<string> 
#include<unistd.h>
#include"Thread.hpp"std::string GetThreadName()
{static int num = 1;char name[64];snprintf(name,sizeof(name),"Thread-%d",num++);return name;
}void Print(int num)
{while(num){std::cout<<"hello world: "<<num--<<std::endl;sleep(1);}
}int ticket = 10000;//全局的共享资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;void GetTicket(pthread_mutex_t* mutex)
{while(true){pthread_mutex_lock(mutex);if(ticket>0){usleep(1000);printf("get a ticket: %d\n",ticket);ticket--;pthread_mutex_unlock(mutex);}else {pthread_mutex_unlock(mutex);break;}}
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex,nullptr);std::string name1 = GetThreadName();Thread<pthread_mutex_t *>t1(name1,GetTicket,&mutex);std::string name2 = GetThreadName();Thread<pthread_mutex_t *>t2(name2,GetTicket,&mutex);std::string name3 = GetThreadName();Thread<pthread_mutex_t *>t3(name3,GetTicket,&mutex);std::string name4 = GetThreadName();Thread<pthread_mutex_t *>t4(name4,GetTicket,&mutex);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();pthread_mutex_destroy(&mutex);return 0;
}

Thread.hpp

#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<pthread.h>
template<class T>
using func_t = std::function<void(T)>;template<class T>
class Thread
{
public:Thread(const std::string & threadname,func_t<T> func,T data):_tid(0),_isrunning(false),_threadname(threadname),_func(func),_data(data){}static void* ThreadRoutine(void * args){Thread* ts =static_cast<Thread*>(args);ts->_func(ts->_data);return nullptr;}bool Start(){int n = pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n==0){_isrunning = true;return true;}return false;}bool Join(){if(!_isrunning) return true;int n = pthread_join(_tid,nullptr);if(n==0){_isrunning = false;return true;}return false;}   std::string ThreadName(){return _threadname;}bool IsRunning(){return _isrunning;}~Thread(){}private:pthread_t _tid;bool _isrunning;std::string _threadname;func_t<T> _func;T _data;
};

运行结果:

一样可以解决票数减到负数的情况,只不过执行变慢了,因为执行临界区的代码那几个线程都是通过串行执行的。

下面我们添加一些代码对锁进行简单的封装:

LockGuard.hpp

#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
{
public:LockGuard(pthread_mutex_t * lock):_mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex _mutex;
};

main.cc

#include<iostream>
#include<string> 
#include<unistd.h>
#include"Thread.hpp"
#include"LockGuard.hpp"
std::string GetThreadName()
{static int num = 1;char name[64];snprintf(name,sizeof(name),"Thread-%d",num++);return name;
}void Print(int num)
{while(num){std::cout<<"hello world: "<<num--<<std::endl;sleep(1);}
}int ticket = 10000;//全局的共享资源
//pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
class ThreadData
{
public:ThreadData(const std::string & name,pthread_mutex_t* lock): threadname(name),pmutex(lock){}pthread_mutex_t * getPmutex(){return this->pmutex;}void setPmutex(pthread_mutex_t * mutex){this->pmutex = mutex;}std::string getName(){return this->threadname;}void setName(std::string name){this->threadname = name;}private:std::string threadname;pthread_mutex_t * pmutex;
};void GetTicket(ThreadData* td)
{while(true){LockGuard lockguard(td->getPmutex());//pthread_mutex_lock(mutex);if(ticket>0){usleep(1000);printf("%s get a ticket: %d\n",td->getName().c_str(),ticket);ticket--;//pthread_mutex_unlock(mutex);}else {//pthread_mutex_unlock(mutex);break;}}
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex,nullptr);std::string name1 = GetThreadName();ThreadData* td1 = new ThreadData(name1,&mutex);Thread<ThreadData*>t1(name1,GetTicket,td1);std::string name2 = GetThreadName();ThreadData* td2 = new ThreadData(name2,&mutex);Thread<ThreadData*>t2(name2,GetTicket,td2);std::string name3 = GetThreadName();ThreadData* td3 = new ThreadData(name3,&mutex);Thread<ThreadData*>t3(name3,GetTicket,td3);std::string name4 = GetThreadName();ThreadData* td4 = new ThreadData(name4,&mutex);Thread<ThreadData*>t4(name4,GetTicket,td4);t1.Start();t2.Start();t3.Start();t4.Start();t1.Join();t2.Join();t3.Join();t4.Join();pthread_mutex_destroy(&mutex);delete td1;delete td2;delete td3;delete td4;return 0;
}

thread.hpp

#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<pthread.h>
template<class T>
using func_t = std::function<void(T)>;template<class T>
class Thread
{
public:Thread(const std::string & threadname,func_t<T> func,T data):_tid(0),_isrunning(false),_threadname(threadname),_func(func),_data(data){}static void* ThreadRoutine(void * args){Thread* ts =static_cast<Thread*>(args);ts->_func(ts->_data);return nullptr;}bool Start(){int n = pthread_create(&_tid,nullptr,ThreadRoutine,this);if(n==0){_isrunning = true;return true;}return false;}bool Join(){if(!_isrunning) return true;int n = pthread_join(_tid,nullptr);if(n==0){_isrunning = false;return true;}return false;}   std::string ThreadName(){return _threadname;}bool IsRunning(){return _isrunning;}~Thread(){}private:pthread_t _tid;bool _isrunning;std::string _threadname;func_t<T> _func;T _data;
};

运行结果:

还是能够正常将票数减到1,但是我们发现一个问题,后面这么多张票都被Thread-3线程给抢了,多线程运行,同一份资源有线程长时间无法拥有,我们把这种现象称之为饥饿问题,单纯的互斥是解决不了的,要解决饥饿问题就要让线程执行的时候具备一定的顺序性---同步。

2.3理解锁

线程加锁的本质:

  • 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
  • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下

这里的定义的mutex是内存级的,线程共享的,而寄存器硬件在CPU内只有一套,但是寄存器的内容有很多套,因为每一个线程都有一份属于自己的上下文!xchgb的作用:将一个共享的mutex资源交换到自己的上下文当中,属于线程自己!上述的指令每一条都是原子的,所以也就保证了加锁和解锁都是原子的。

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

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

相关文章

Java 实现缓存的三种方式

Java 实现缓存的三种方式 文章目录 Java 实现缓存的三种方式一、HashMap实现缓存Step-1&#xff1a;实现一个缓存管理类Step-2&#xff1a;将缓存管理类交给 Spring 进行管理Step-3&#xff1a;编写接口测试缓存Step-4&#xff1a;结果展示 二、guava local cache 实现Step-1&a…

为什么Python不适合写游戏?

知乎上有热门个问题&#xff1a;Python 能写游戏吗&#xff1f;有没有什么开源项目&#xff1f; Python可以开发游戏&#xff0c;但不是好的选择 Python作为脚本语言&#xff0c;一般很少用来开发游戏&#xff0c;但也有不少大型游戏有Python的身影&#xff0c;比如&#xff1…

【Linux】详解进程程序替换

一、替换原理 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支)&#xff0c;子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时&#xff0c;该进程的用户空间代码和数据完全被新程序替换&#xff0c;从新程序的启动例程开始执…

Mysql数据库——高级SQL语句补充

目录 一、子查询——Subquery 1.环境准备 2.In——查询已知的值的数据记录 2.1子查询——Insert 2.2子查询——Update 2.3子查询——Delete 3.Not In——表示否定&#xff0c;不在子查询的结果集里 3.Exists——判断查询结果集是否为空 4.子查询——别名 二、视图—…

Channel 阻塞机制、死锁问题

Channel 阻塞机制 在Go语言中&#xff0c;channel是用于在不同的goroutine之间进行通信的主要机制。channel的阻塞机制确保了数据的同步传输&#xff0c;这意味着在某些情况下&#xff0c;操作channel的goroutine可能会被挂起&#xff08;阻塞&#xff09;&#xff0c;直到另一…

C 传递数组给函数

如果您想要在函数中传递一个一维数组作为参数&#xff0c;您必须以下面三种方式来声明函数形式参数&#xff0c;这三种声明方式的结果是一样的&#xff0c;因为每种方式都会告诉编译器将要接收一个整型指针。同样地&#xff0c;您也可以传递一个多维数组作为形式参数。 方式 1…

PointNet++点云处理原理

PointNet点云处理原理 借鉴了多层神经网络的思想 pointnet要么是一个点&#xff0c;要么是所有点进行操作&#xff0c;就不会有局部上下文信息 pointnet基本思想是迭代地应用到局部区域 1.多级别特征学习 2.旋转不变性 3.置换不变性 选取中心点centroid&#xff0c;通过poi…

《c++》纯虚函数和抽象类

在C中&#xff0c;纯虚函数和抽象类是面向对象编程中的重要概念&#xff0c;用于实现多态性和接口定义 1.纯虚函数&#xff08;Pure Virtual Function&#xff09;&#xff1a; 纯虚函数是在基类中声明的虚函数&#xff0c;但没有提供实现。它们以关键字声明&#xff0c;并在函…

jconsole jvisualvm

jconsole 打开方式 命令行输入 jconsole双击想要连接的应用 界面展示 jvisualvm 打开方式 命令行输入 jvisualvm双击想要连接的应用 可以安装插件&#xff0c;比如 Visual GC 直观看到 GC 过程

在CentOS7上部署Nginx并测试指南

Nginx部署测试 Nginx简介 Nginx是俄罗斯人Igor Sysoev编写的一款高性能的HTTP和反向代理服务器。 Nginx选择了epoll和kqueue作为网络I/O模型&#xff0c;在高连接并发的情况下&#xff0c;内存、CPU等系统资源消耗非常低&#xff0c;运行稳定。 正向代理与反向代理 正向代…

Java学习记录第十三天

面向对象编程 核心思想就是OOP&#xff08;面向对象编程&#xff09; 面向过程&面向对象 面向过程思想 步骤清晰简单&#xff0c;第一步做什么&#xff0c;第二步做什么... 面对过程适合处理一些较为简单的问题 面向对象思想 物以类聚&#xff0c;分类的思维模式&…

Redis慢日志!!!

用途&#xff1a;系统优化&#xff0c;将执行较慢的sql记录下来&#xff0c;然后对其进行优化。 1.慢查询日志的两个配置项 slowlog-log-slower-than Redis 慢查询日志的时间阈值&#xff0c;单位微妙。 1) 值为正数&#xff0c;执行时间大于该值设置的微秒时才记录到慢日志中…

电源噪声的起因及危害

对造成电源不稳定的根源进行简单分析如下,主要在于两个方面:一是器件高速开关状态下,瞬态的交变电流过大;二是电流回路上存在的电感。从表现形式上来看又可以分为三类:同步开关噪声(SSN),有时被称为Δi噪声,地弹(Ground bounce)现象也可归于此类(图1-a);非理想电…

2024.3.21 QT

QT登录界面设计&#xff1a; //头文件 #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QMovie>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : public QWidget {Q_OBJECTpublic:Widget(QWidget *parent nu…

Qt——Qt文本读写之QFile与QTextStream的使用总结(打开文本文件,修改内容后保存至该文件中)

【系列专栏】:博主结合工作实践输出的,解决实际问题的专栏,朋友们看过来! 《项目案例分享》 《极客DIY开源分享》 《嵌入式通用开发实战》 《C++语言开发基础总结》 《从0到1学习嵌入式Linux开发》 《QT开发实战》 《Android开发实战》

【AI】安装web UI时总是报找不到yaml

【背景】 text generation web ui是为本地hosting提供AI服务而出现的前端框架&#xff0c;后端可以自由下载hugging face上的model&#xff0c;load后就可以直接在Web UI上访问。这个服务可以自由架构在局域网上供内网用户访问。 我觉得挺满足自己需要&#xff0c;就在windows…

Linux manim安装

简介 根据文档可知, manim目前分为两个版本, 一个是由3Blue1Brown维护更新的最新版本的manimgl, 另一个是稳定的社区版本manim or manimce. 两个版本在安装和使用上都有些不同, 不要搞混. Linux manim ERROR No package ‘pangocairo’ found Getting requirements to buil…

C++进阶之路---C++11新特性 | lambda表达式

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C从入门到精通》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 前言&#xff1a;简介lambda 在C中&#xff0c;lambda表达式是一种匿名函数的方式&#xff0c;它可以用来解决以下问题&a…

稀碎从零算法笔记Day26-LeetCode:跳跃游戏

断更多天&#xff0c;懒狗ex 题型&#xff1a;数组、模拟、类似双指针&#xff1f; 链接&#xff1a;55. 跳跃游戏 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1a;LeetCode 题目描述 给你一个非负整数数组 nums &#xff0c;你最初位于数组的 第一个下标 。数组…

【Diffusers库】第四篇 训练一个扩散模型(Unconditional)

目录 写在前面的话下载数据模型配置文件加载数据创建一个UNet2DModel创建一个调度器训练模型完整版代码&#xff1a; 写在前面的话 这是我们研发的用于 消费决策的AI助理 &#xff0c;我们会持续优化&#xff0c;欢迎体验与反馈。微信扫描二维码&#xff0c;添加即可。   官方…