Linux学习之路 -- 线程 -- 互斥

news/2024/9/23 5:18:42/

目录

1、概念引入

2、互斥锁

1、pthread_mutex_init && pthread_ mutex_destory 

2、pthread_mutex_lock && pthread_mutex_unlock

3、互斥锁原理的简单介绍


1、概念引入

为了介绍线程的同步与互斥,我们以抢票逻辑引入相关的概念。

示例代码:

#include<iostream>
#include<vector>int g_ticket = 1000;void* funtion(void* arg)
{while(1){if(g_ticket > 0){//模拟抢票逻辑std::cout << "g_ticket : " << g_ticket << " &g_ticket : " << &g_ticket << std::endl;g_ticket--;}else{break;}}return nullptr;
}
const int num = 5;
int main()
{std::vector<pthread_t> pthread;for(int i = 0; i < num; i++){pthread_t id;pthread_create(&id,nullptr,funtion, nullptr);pthread.emplace_back(id);}for(auto& e: pthread){pthread_join(e,nullptr);}return 0;
}

我们创建多个线程,然后让不同的线程对同一个变量进行操作。也就是让不同线程访问同一个函数,抢票函数会让票数减减。我们运行一下代码,观察一下结果。

这里我们可以看见,g_ticket的地址是一样的,说明访问的变量一定是同一全局变量。而g_ticket的数值却出现-2,-3。这明显是不符合我们的要求的,所以这里一定是有问题的,而这也叫数据不一致 。

首先解释一下为什么会造成这种现象,首先if中的条件判断是逻辑判断,这是要在cpu内进行的运算的。(我们假设g_ticket的值为1)

每当一个线程在将内存中g_ticket数据放入cpu中的寄存器,由cpu执行判断逻辑运算。在判断成功后,该线程就有可能直接被切走了,没有执行到下面的g_ticket--操作,而内存中的g_ticket还是1。下个线程也会重复上述的操作,这就导致虽然只有一张票,但却还是有多个线程进入该函数中。线程同时执行了ticket--操作,所以就会造成ticket被减到负数的情况。同时,需要注意的是,ticket--也不是原子的,这段代码在cpu中的执行过程分为三步,第一步是将内存中g_ticket读取到cpu中,第二步是将cpu中的g_ticket做减减操作,第三步是将cpu中的值写回内存中。这里的每一步在结束后,线程可能都会被切走,这也会造成g_ticket这个数据不安全,不过这里一般出错概率较低,主要还是上述因素造成的。这种因为原子性问题导致数据不一致情况还是比较常见的。(原子性:只有执行和没执行两种状态,说通俗一点就是翻译成汇编只有一条语句,上面的++操作翻译成汇编就有3条语句) 总结一下,这里g_ticket出现负数情况的原因,其实就是这个共享资源没有被保护,并且访问该共享资源的过程并不是原子性的。

2、互斥锁

如何解决上述的问题呢?这里我们就需要引入锁的概念了。原生线程库不仅提供了线程创建等相关的接口,还提供了互斥锁的相关接口。我们在线程中常用的锁一般称为互斥锁,互斥的概念前面已经有所介绍,这里不再赘述。我们只要对共享资源进行加锁,就能防止出现上述的问题。下面先介绍一下相关的接口。

1、pthread_mutex_init && pthread_ mutex_destory 

在使用锁之前,我们首先需要定义一个pthread_mutex_t类型的变量。如果这个变量是局部变量,我们就需要使用pthread_mutex_init进行初始化(这里的第二参数表示锁的属性,这里定义成nullptr即可),同时在使用完后,要对锁进行pthread_mutex_destory释放。如果这个变量是全局变量,则我们只需要在定义变量时,让其等于PTHREAD_MUTEX_INITALIZER进行初始化即可,后面不需要手动进行销毁。


2、pthread_mutex_lock && pthread_mutex_unlock

当我们初始化锁以后,我们就需要使用锁了,pthread_mutex_lock 就表示申请上锁,申请成功,函数返回,继续向后执行;申请失败,一直阻塞直至申请成功;如果函数调用失败,出错返回。而trylock接口在申请失败后会直接返回,这是和lock接口的区别。而unlock就表示解开该锁。

下面演示一下加锁例子,我们以上面抢票逻辑的代码为例子。

ThreadMode.hpp

#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{template<typename T>using func_t = std::function<void(T)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:void Excute(){_func(_data);}public:Thread(func_t<T> func, T data, const std::string &name="none-name")//右值: _func(func), _data(data), _threadname(name), _stop(true){}static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!{Thread<T> *self = static_cast<Thread<T> *>(args);self->Excute();return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n){_stop = false;return true;}else{return false;}}void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}T& Data(){return _data;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data;  // 为了让所有的线程访问同一个全局变量func_t<T> _func;bool _stop;};
} // namespace ThreadModule#endif
#include <iostream>
#include "ThreadMode.hpp"
#include <vector>int g_ticket = 1000;
using namespace ThreadModule;
template <class T>
class ThreadData
{
public:ThreadData(int &data, const std::string str) : _data(data), name(str), total(0){}~ThreadData(){}std::string Getname(){return name;}void buyticket(){_data--;}int Geticket(){return _data;}void Plus(){total++;}void Total(){std::cout << name << " : " << total << std::endl;}private:int &_data;std::string name;int total;
};
pthread_mutex_t _lock = PTHREAD_MUTEX_INITIALIZER;//全局锁
void funtion(ThreadData<int> *td)
{while (1){//加锁pthread_mutex_lock(&_lock);if (g_ticket > 0){std::cout << td->Getname() << " get ticket, remain ticket number: " << td->Geticket() << std::endl;td->buyticket();pthread_mutex_unlock(&_lock);td->Plus();}else{pthread_mutex_unlock(&_lock);break;}}
}
const int num = 5;
int main()
{std::vector<Thread<ThreadData<int> *>> thread;for (int i = 0; i < num; i++){// char* threadname = new char[64];// snprintf(threadname, 64, "Thread-%d", i + 1);std::string threadname = "thread -" + std::to_string(i + 1);ThreadData<int> *ptr = new ThreadData<int>(g_ticket, threadname);thread.emplace_back(Thread<ThreadData<int> *>(funtion, ptr, threadname));}for (auto &e : thread){e.Start();}for (auto &e : thread){sleep(1);e.Data()->Total();e.Join();delete e.Data();}return 0;}

这里我们着重看funtion执行函数即可,在该函数外部,我定义了一把全局锁。当不同线程执行同一函数,需要访问一块共享资源(临界资源)的代码,我们就称为临界区,其它部分,我们称为非临界区。而我们要保护的,其实就是临界区的资源,这就要求我们在加锁时,尽量只要对临界区的部分进行加锁即可,对其他非临界区的部分,可以不用管。这里当多个线程同时访问同一变量时,就需要去竞争那一把全局锁。谁竞争到了锁,谁就能对临界资源进行访问,其余线程只能在临界区外进行等待。当然,这里不排除有的线程竞争锁的能力很强,让其他线程根本就竞争不到锁的情况,这就会造成其他进程的饥饿问题。在不同系统下,不同线程的竞争能力不同,这和锁的创建时间、os的调度算法等有管。

运行结果

这里就出现了线程4、5的竞争问题,不过这里,我们暂不探究。而票数这个全局变量,在加锁后,就回复正常了。在C++中,对互斥锁的释放和初始化等等操作进行了包装,这里不一一介绍,如果有需要,也可以自行封装。

3、互斥锁原理的简单介绍

互斥锁底层在不同OS下可能有不同的实现方式,这里简单介绍一种。在互斥锁中,实际上表示持有锁的状态就是用一个整数,我们可以用+或-来改变其状态,但我们前面提到,++操作并不是原子性的,所以这个用整数来表示持有锁的状态是有一定问题的。为了解决该问题,系统中有特定的汇编指令,能够原子地交换cpu寄存器和物理内存中的数值。我们用1表示持有锁,0表示不持有锁。

假设我们现在定义了一把锁,我们lock这个整型变量来表示锁地使用情况。如果lock为0,表示锁被使用,不为零,就被没使用。在内存中的数据大部分是被线程共享的,而在cpu上的寄存器中存储的硬件上下文是被线程私有的。

首先,线程内部也有整型变量表示是否持有锁,我们以0表示不持有锁,1表示持有锁。以上图为例,thread-1申请锁时,会首先将线程内部的变量读入寄存器中,然后通过特殊汇编指令与内存中的lock值交换(该过程为原子的),此时就完成了锁的申请。此时时间片耗尽,线程就会切换,同时寄存器中的硬件上下文也会被清空,锁被带走。第二个线程也会将自己表示持有锁状态的变量读入寄存器中,然后重复上述的动作,但是由于lock变量已经为零,所以第二个线程即使交换完后,也是无法持有锁的。解锁,就是把线程上的数据交换会内存中,表现在图上就是线程中的“1”换回内存中lock变量。所以其他线程想要访问临界资源,就只能等待线程把锁释放,访问临界区的过程也就是线程安全的。

以上就是所有的内容,文中如有不对之处,还望各位大佬指正,谢谢!!!


http://www.ppmy.cn/news/1529170.html

相关文章

YOLOv10改进,YOLOv10替换主干网络为PP-HGNetV2(百度飞桨视觉团队自研,独家手把手教程,助力涨点)

摘要 PP-HGNetV2(High Performance GPU Network V2) 是百度飞桨视觉团队自研的 PP-HGNet 的下一代版本,其在 PP-HGNet 的基础上,做了进一步优化和改进,最终在 NVIDIA GPU 设备上,将 “Accuracy-Latency Balance” 做到了极致,精度大幅超过了其他同样推理速度的模型。其在…

Windows上,使用远程桌面连接Ubuntu

要在 Ubuntu 上设置公网 IP 并通过 Windows 远程桌面连接到 Ubuntu&#xff0c;你需要完成以下步骤&#xff1a; 设置 Ubuntu 公网 IP&#xff1a; 确保你的 Ubuntu 服务器已经配置了一个公网 IP 地址。 你可以通过云服务提供商&#xff08;如 AWS、Azure、Google Cloud&#…

鸿蒙4.0(HarmonyOS 4.0)与鸿蒙Next(HarmonyOS Next)区别

鸿蒙4.0&#xff08;HarmonyOS 4.0&#xff09;与鸿蒙Next&#xff08;HarmonyOS Next&#xff09;是华为推出的两个不同版本的操作系统&#xff0c;它们之间存在一些显著的区别&#xff1a; 兼容性&#xff1a; 鸿蒙4.0&#xff1a;依然保持了对Android应用的兼容性&#xff0…

《重生之我在java世界做任务升级》--第一章

ps:此乃我学习《Head First Java》之后的一本心得体会&#xff0c;现其分享给各位行走在Java道路上的道友 第一章&#xff1a;进入java的世界 尊敬的java玩家&#xff0c;欢迎来到JavaWorld&#xff0c;我将根据您的外貌特征为您创建游戏角色。 一眨眼&#xff0c;我仿佛进入了…

shell脚本(9.20)

1、 写一个shel脚本&#xff0c;将以下内容放到脚本中 a.在家目录下创建目录文件&#xff0c;dir b.dir下创建dir1和dir2 c.把当前目录下的所有文件拷贝到dir1中 d.把当前目录下的所有脚本文件拷贝到dir2中 e.把dir2打包并压缩为dir2.tar.xz f.再把dir2.tar.xz移动到dir1中 g.解…

linux下共享内存的3种使用方式

进程是资源封装的单位&#xff0c;内存就是进程所封装的资源的一种。一般情况下&#xff0c;进程间的内存是相互隔离的&#xff0c;也就是说一个进程不能访问另一个进程的内存。如果一个进程想要访问另一个进程的内存&#xff0c;那么必须要进过内核这个桥梁&#xff0c;这就是…

C++中string类的模拟实现

目录 1.string类的结构 2.默认成员函数 2.1.默认构造函数 2.2拷贝构造函数 2.3赋值运算符重载 2.4析构函数 3.迭代器(Iterators) 4.string类的空间操作(Capacity) 4.1size() 4.2capacity() 4.3clear() 4.4reserve() 5.元素访问(Element access) 6.string类的修…

LeetCode_sql_day31(1384.按年度列出销售总额)

目录 描述 1384.按年度列出销售总额 数据准备 分析 法一 法二 代码 总结 描述 1384.按年度列出销售总额 Product 表&#xff1a; ------------------------ | Column Name | Type | ------------------------ | product_id | int | | product_name | var…