【Linux】线程安全的艺术:解锁互斥量在并发编程中的应用

文章目录

  • 前言:
  • 1. 进程线程间的互斥相关背景概念
    • 1.1. 操作共享变量会有问题的售票系统代码:
  • 2. 互斥量的接口
    • 2.1. 解决方案
      • 2.1.1. 使用全局的锁:
      • 2.1.2. 使用局部的锁:
      • 2.1.3. 封装为RAII风格的加锁和解锁:
      • 2.1.4. C++ 11 中也有类似的锁:
  • 3. 互斥的底层实现
  • 总结:

前言:

在现代的操作系统中,多线程编程是一种常见的并发执行方式,它能够提高程序的执行效率和资源利用率。然而,当多个线程需要访问同一资源时,如果没有适当的同步机制,就可能出现数据竞争、条件竞争等并发问题,导致程序运行结果不可预测。本文将深入探讨进程和线程间互斥的背景概念,介绍互斥量(mutex)的使用和实现原理,并提供具体的编程示例,以帮助读者理解和掌握多线程编程中的互斥机制。

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

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

互斥量mutex

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

1.1. 操作共享变量会有问题的售票系统代码:

// Thread.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;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data;  // 为了让所有的线程访问同一个全局变量func_t<T> _func;bool _stop;};
} // namespace ThreadModule#endif
// Thread.cc
#include <iostream>
#include <vector>
#include "Thread.hpp"using namespace ThreadModule;int g_tickets = 10000; //一万张票,共享资源,没有被保护的,//对全局的tickets的判断不是原子的!
const int num = 4; // 创建4个进程class threadData
{
public:threadData(int& tickets, const std::string& name) :_tickets(tickets), _name(name), _total(0){}~threadData(){}
public:int &_tickets;  // 所有的线程,最后都会引用同一个全局的g_ticketsstd::string _name;int _total;
};void route(threadData* td)
{while(true){if (td->_tickets > 0){// 模拟一次抢票的逻辑usleep(1000);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);td->_tickets--;td->_total++;}else{break;}}
}int main()
{//std::cout << "main: &tickets:" << &g_tickets << std::endl;std::vector<Thread<threadData*>> threads;std::vector<threadData*> datas;// 1. 创建一批线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);threadData* td = new threadData(g_tickets, name);threads.emplace_back(route, td, name);datas.emplace_back(td);}// 2. 启动 一批线程for (auto &thread : threads){thread.Start();}// 3. 等待一批线程for (auto &thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}sleep(1);// 4. 输出统计数据for (auto& data : datas){std::cout << data->_name << " : " << data->_total << std::endl;}return 0;
}

在这里插入图片描述
为什么抢到了负数:

   if (td->_tickets > 0)

判断是逻辑运算,必须在CPU内部运行。

td->_tickets--;  

tickets 等价于 tickets = tickets-1;

  1. 从内存读取到CPU
  2. CPU内部进行-- 操作
  3. 写回内存

它不是原子的,编译后不止一条汇编语句

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

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

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
在这里插入图片描述
初始化互斥量
初始化互斥量有两种方法:

解决问题:

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

销毁互斥量
销毁互斥量需要注意:

  • 使用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调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

2.1. 解决方案

出现并发访问的问题,本质是因为多个执行流的访问全局数据的代码导致! 
保护全局共享资源,本质是通过保护:临界区完成的!

临界区:这段代码在执行时访问共享资源(如共享内存、全局变量等),而且可能会被多个并发执行的线程或进程访问。
非临界区:不访问共享资源的代码段,或者即使访问了也不会引发竞争条件的代码段。

我们加锁,本质就是把并行执行变为串行执行,加锁的粒度越细越好!

2.1.1. 使用全局的锁:

// tesetThread.cc
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;void route(threadData* td)
{while(true){// 访问临界资源的代码,叫做临界区!我们加锁,本质就是把并行执行变为串行执行,加锁的粒度越细越好!pthread_mutex_lock(&gmutex); // 加锁:竞争是自由竞争的,竞争锁的能力太强的线程,会导致其它线程抢不到锁,造成了其它写线程的饥饿问题!if (td->_tickets > 0) // 1{// 模拟一次抢票的逻辑usleep(1000);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); // 2td->_tickets--;															 // 3pthread_mutex_unlock(&gmutex); // 解锁td->_total++; // 每个线程一人一个不属于临界区}else{pthread_mutex_unlock(&gmutex); // 解锁break;}}
}

在这里插入图片描述
加锁:竞争是自由竞争的,竞争锁的能力太强的线程,会导致其它线程抢不到锁,造成了其它写线程的饥饿问题!

2.1.2. 使用局部的锁:

// tesetThread.cc
#include <iostream>
#include <vector>
#include "Thread.hpp"using namespace ThreadModule;// 共享资源没有被保护,临界资源
int g_tickets = 10000; //一万张票,共享资源,没有被保护的,对全局的tickets的判断不是原子的!const int num = 4; // 创建4个进程class threadData
{
public:threadData(int& tickets, const std::string& name, pthread_mutex_t &mutex) :_tickets(tickets), _name(name), _total(0), _mutex(mutex){}~threadData(){}
public:int &_tickets;  // 所有的线程,最后都会引用同一个全局的 g_ticketsstd::string _name;int _total;pthread_mutex_t &_mutex;
};// pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;void route(threadData* td)
{while(true){// 访问临界资源的代码,叫做临界区!我们加锁,本质就是把并行执行变为串行执行,加锁的粒度越细越好!// pthread_mutex_lock(&gmutex); // 加锁pthread_mutex_lock(&td->_mutex);if (td->_tickets > 0){// 模拟一次抢票的逻辑usleep(100);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);td->_tickets--;// pthread_mutex_unlock(&gmutex); // 解锁pthread_mutex_unlock(&td->_mutex);td->_total++; // 每个线程一人一个不属于临界区}else{// pthread_mutex_unlock(&gmutex); // 解锁pthread_mutex_unlock(&td->_mutex);break;}}
}int main()
{//std::cout << "main: &tickets:" << &g_tickets << std::endl;pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);std::vector<Thread<threadData*>> threads;std::vector<threadData*> datas;// 1. 创建一批线程for (int i = 0; i < num; ++i){std::string name = "thread-" + std::to_string(i + 1);threadData* td = new threadData(g_tickets, name, mutex);threads.emplace_back(route, td, name);datas.emplace_back(td);}// 2. 启动 一批线程for (auto &thread : threads){thread.Start();}// 3. 等待一批线程for (auto &thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}sleep(1);// 4. 输出统计数据for (auto& data : datas){std::cout << data->_name << " : " << data->_total << std::endl;}return 0;
}

上述我们这种现象叫做互斥,可以保证不出错。
互斥这套规则,必须被所有访问临界区的线程遵守!

2.1.3. 封装为RAII风格的加锁和解锁:

// LockGuard.hpp
#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__#include <iostream>
#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){pthread_mutex_lock(_mutex); // 构造加锁}~LockGuard(){pthread_mutex_unlock(_mutex);}private:pthread_mutex_t* _mutex;
};#endif
// tesetThread.cc
void route(threadData* td)
{while(true){LockGuard guard(&td->_mutex); // 临时对象,RAII风格的加锁和解锁if (td->_tickets > 0){usleep(500);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);td->_tickets--;td->_total++;}else{break;} }
}

2.1.4. C++ 11 中也有类似的锁:

#include <mutex>int main()
{std::mutex mutex;// ...
}void route(threadData* td)
{while(true){td->_mutex.lock();if (td->_tickets > 0){usleep(500);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);td->_tickets--;td->_mutex.unlock();td->_total++;}else{td->_mutex.unlock();break;} }
}
void route(threadData* td)
{while(true){std::lock_guard<std::mutex> lock(td->_mutex);if (td->_tickets > 0){usleep(500);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);td->_tickets--;td->_total++;}else{break;} }
}

3. 互斥的底层实现

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

线程切换的的时机是随机的!
交换的本质:不是拷贝到寄存器,而且所有线程在争锁的时候,只有一个 1
交换的时候,只有一条汇编——原子的!

CPU寄存器硬件只有一套,但是CPU寄存器内部的数据,数据线程的硬件上下文!
数据在内存里,所有线程都能访问,属于共享的,但是如果转移到CPU内部寄存器,就属于一个线程私有数据了!

  • 一个问题?
    临界区内部,正在访问临界区的线程,可不可以被OS切换调度呢?

一个线程正在访问临界区,没有释放锁之前,对于其他线程:

  1. 被锁释放
  2. 曾经我没有申请到锁

所以临界区只要一旦加锁,对于其它线程而言就是原子的,整个抢票过程是线程安全的!

总结:

本文首先介绍了进程和线程间互斥的相关背景概念,包括临界资源、临界区和互斥的概念,以及原子性的重要性。接着,通过一个售票系统的示例代码,展示了在没有同步机制的情况下,多个线程并发访问共享资源时可能出现的问题。文章进一步讨论了互斥量的接口和底层实现,包括互斥量的初始化、加锁、解锁和销毁等操作,并介绍了如何使用全局锁和局部锁来解决并发问题。此外,还介绍了RAII风格的加锁和解锁方法,以及C++ 11中提供的互斥量支持。

通过本文的学习,读者应该能够理解互斥量在多线程编程中的重要性,掌握互斥量的使用方法,并能够运用这些知识来解决实际编程中遇到的并发问题。文章最后指出,临界区的访问必须被加锁保护,以确保线程安全,并且强调了互斥规则必须被所有访问临界区的线程遵守。通过合理地使用互斥量,可以有效地避免数据竞争和条件竞争,确保多线程程序的正确性和稳定性。

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

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

相关文章

Liunx音频

一. echo -e "\a" echo 通过向控制台喇叭设备发送字符来发声&#xff1a; echo -e "\a"&#xff08;这里的 -e 选项允许解释反斜杠转义的字符&#xff0c;而 \a 是一个响铃(bell)字符&#xff09; 二. beep 下载对应的包 yum -y install beep 发声命令 be…

YashanDB携手宏杉科技助力国产软件生态发展

近日&#xff0c;深圳计算科学研究院崖山数据库系统YashanDB与宏杉科技系列存储、系列服务器与数据库一体机等多款产品顺利完成兼容性互认证。经严格测试&#xff0c;双方产品完全兼容&#xff0c;稳定运行&#xff0c;共同提供高效、稳定、安全的国产软硬件一体化解决方案&…

《精通ChatGPT:从入门到大师的Prompt指南》大纲目录

第一部分&#xff1a;入门指南 第1章&#xff1a;认识ChatGPT 1.1 ChatGPT是什么 1.2 ChatGPT的应用领域 1.3 为什么需要了解Prompt 第2章&#xff1a;Prompt的基本概念 2.1 什么是Prompt 2.2 好Prompt的特征 2.3 常见的Prompt类型 第二部分&#xff1a;Prompt设计技巧 第…

解决 iOS 端小程序「saveVideoToPhotosAlbum:fail invalid video」问题

场景复现&#xff1a; const url https://mobvoi-digitalhuman-video-public.weta365.com/1788148372310446080.mp4uni.downloadFile({url,success: (res) > {uni.saveVideoToPhotosAlbum({filePath: res.tempFilePath,success: (res) > {console.log("res > &…

数学题目系列(一)|丑数|各位和|埃氏筛|欧拉筛

一.丑数 链接&#xff1a;丑数 分析&#xff1a; 丑数只有2&#xff0c;3&#xff0c;5这三个质因数&#xff0c;num 2a 3b 5c也就是一个丑数是由若干个2&#xff0c;3&#xff0c;5组成&#xff0c;那么丑数除以这若干个数字最后一定变为1 代码 class Solution {publi…

NocoDB开源的智能表格详解-腾讯文档本地替代品

文章目录 一、介绍二、docker-compose部署三、登录NocoDB四、NocoDB手册1. 创建项目2. 收集统计表2.1 添加字段2.2 编辑字段2.3 字段类型2.4 发布表格 3.创建表单3.1 创建表单3.2 分享表单3.3 填写检测单 4.创建看板5.创建画廊 一、介绍 可作为腾讯文档的本地电子表格替代品&a…

C# BindingSource 未完

数据绑定导航事件数据验证自定义示例示例总结 在 C#中&#xff0c; BindingSource 是一个非常有用的控件&#xff0c;它提供了数据绑定的基础设施。 BindingSource 允许开发者将数据源&#xff08;如数据库、集合、对象等&#xff09;与用户界面控件&#xff08;如文本框、下…

5G+北斗智能手持终端在哪些行业中发挥作用

在当今科技融合发展的浪潮中&#xff0c;5G北斗智能手持终端正逐步成为驱动各行各业智能化升级的关键力量。这一融合创新技术不仅重塑了传统的通信与定位方式&#xff0c;而且在多个核心领域展现了其变革性的应用价值。 5G北斗智能手持终端因其独特的技术组合&#xff0c;在多个…

File类操作文件方法详解及其简单应用

一、File 类介绍 Java 中的 File 类是 java.io 包的一部分&#xff0c;它提供了操作文件和目录的能力。File 类可以用来表示文件系统中的文件或目录。 二、路径 在讲File用法之前咱们先介绍一下路径是什么&#xff1f; 在计算机中&#xff0c;路径&#xff08;Path&#xff0…

kotlin 调用java的get方法Use of getter method instead of property access syntax

调用警告 Person.class public class Person {private String name;Person(String name) {this.name name.trim();}public String getName() {return name;}public void setName(String name) {this.name name;}public String getFullName() {return name " Wang&quo…

【MySQL】数据库入门基础

文章目录 一、数据库的概念1. 什么是数据库2. 主流数据库3. mysql和mysqld的区别 二、MySQL基本使用1. 安装MySQL服务器在 CentOS 上安装 MySQL 服务器在 Ubuntu 上安装 MySQL 服务器验证安装 2. 服务器管理启动服务器查看服务器连接服务器停止服务器重启服务器 3. 服务器&…

麒麟操作系统rpm ivh安装rpm包卡死问题分析

夜间变更开发反应,rpm -ivh 安装包命令夯死,无执行结果,也无报错 排查 : 1、top 查看无进程占用较高进程存在,整体运行平稳 2、df -h 查看磁盘并未占满 3、其他服务器复现该命令正常执行 4、ps -ef|grep rpm 查看安装命令仍在运行中 5、查看log日志,均正常并无不良日志…

UE Editor API 整理

UE Editor API 整理 过一下 https://github.com/20tab/UnrealEnginePython/blob/master/docs/&#xff0c;熟悉一下编辑器 API&#xff0c;方便后续编辑器脚本开发 后续的目标是所有编辑器操作应该都可以脚本化&#xff08;自动化&#xff09;&#xff0c;这样把 GPT 接进 UE…

了解Kubernetes-RKE2的PKI以及证书存放位置

一、什么是PKI&#xff1f; 简称&#xff1a;证书基础设施。 可以方便理解为当你的集群有Server,Client架构&#xff0c;那么为了安全加密之间的通信&#xff0c;则需要使用证书进行交互&#xff0c;那么利用PKI架构可以安全加密组件之间的通信。 二、Kubernetes的PKI架构什…

HLA高层体系结构1.0.0版本

名&#xff1a;高层体系结构&#xff08;High Level Architecture&#xff0c;HLA&#xff09; 高层体系结构&#xff08;High Level Architecture&#xff0c;HLA&#xff09;是从体系结构上建立这样一个框架&#xff0c;它能尽量涵盖M&S领域中所涉及的各种不同类型的仿真…

代码随想录算法训练营第十四天| 104.二叉树的最大深度 、 111.二叉树的最小深度、 222.完全二叉树的节点个数

104.二叉树的最大深度 题目链接&#xff1a;二叉树的最大深度 文档讲解&#xff1a;代码随想录 状态&#xff1a;so easy 思路&#xff1a;左子树和右子树中取最大深度&#xff0c;依次往下递归 递归解法&#xff1a; public int maxDepth(TreeNode root) {if (root null) {r…

【高校科研前沿】新疆生地所陈亚宁研究员团队在GeoSus发文:在1.5°C和2°C全球升温情景下,中亚地区暴露于极端降水的人口增加

目录 文章简介 1.研究内容 2.相关图件 3.文章引用 文章简介 论文名称&#xff1a;Increased population exposures to extreme precipitation in Central Asia under 1.5 ◦C and 2 ◦C global warming scenarios&#xff08;在1.5C和2C全球变暖情景下&#xff0c;中亚地区…

伽马校正技术在AI绘画中的作用

随着人工智能技术的飞速发展&#xff0c;AI绘画已经成为了艺术创作领域的一股新兴力量。在这个数字化时代&#xff0c;计算机图形学和机器学习的结合为我们带来了前所未有的创作工具。然而&#xff0c;为了实现更加真实和自然的色彩表现&#xff0c;伽马校正技术在其中扮演着至…

DP读书:如何使用badge?(开源项目下的标咋用)

最近在冲论坛&#xff0c;很少更一些内容了。但遇到了一个真的有趣的&#xff1a; 开源项目下&#xff0c;蓝蓝绿绿的标是怎么用的呢&#xff1f; 这是我的主页Readme&#xff0c;在看一些NXP的主仓时&#xff0c;突然发现没有这个玩&#xff0c;就自己整了个 再比如我的CSDN专…

Vivado 设置关联使用第三方仿真软件 Modelsim

目录 1.前言2.Vivado 设置关联使用第三方仿真软件 Modelsim 微信公众号获取更多FPGA相关源码&#xff1a; 1.前言 Vivado 软件自带有仿真功能,该功能使用还是比较方便的,初学者可以直接使用自带的仿真功能。 Modelsim仿真工具是Model公司开发的。它支持Verilog、VHDL以及他…