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

news2024/12/23 4:32:18

文章目录

  • 前言:
  • 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_tickets
    std::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); // 2
            td->_tickets--;															 // 3
            pthread_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_tickets
    std::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.coloradmin.cn/o/1796639.html

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

相关文章

LLVM后端__llc中值定义信息的查询方法示例

关于LiveIntervals pass中相关数据结构的含义&#xff0c;在寄存器分配前置分析(5.1) - LiveInterval这篇博客中已经做了清晰的讲解&#xff0c;此处不再赘述&#xff0c;本文主要讲解值定义信息VNInfo的使用方法和注意事项。 1. VNInfo含义 在LLVM的源码中&#xff0c;VNInf…

Rustdesk 自建服务器教程

一、环境 阿里云轻量服务器、debian11 系统 二、服务端搭建 2.1、开放防火墙指定端口 TCP(21115, 21116, 21117, 21118, 21119)UDP(21116) 2.2、安装 rustdesk 服务器文件 在 github 下载页https://github.com/rustdesk/rustdesk-server/releases/&#xff0c;下载 rustde…

(VS2019+VTK9.3)第一个VTK程序

经过配置后&#xff0c;VS2019VTK 9.3&#xff0c;第一个VTK程序&#xff0c;可显示。 #include <vtkAutoInit.h> #include <vtkActor.h> #include <vtkCamera.h> #include <vtkCylinderSource.h> #include <vtkNamedColors.h> #include <vt…

【十大排序算法】选择排序

选择就像是在谱曲&#xff0c;每个决定就是一个音符&#xff0c;只有将它们有序地安排在一起&#xff0c;才能奏响美妙的乐章。 文章目录 一、选择排序的思想二、选择排序的发展历程三、选择排序具象化四、选择排序算法实现五、选择排序的特性推荐阅读 一、选择排序的思想 选…

快速排序(Quick Sort)(C语言) 超详细解析!!!

生活的本质是什么呢? 无非就是你要什么就不给你什么. 而生活的智慧是什么呢? 是给你什么就用好什么. ---马斯克 索引 一. 前言二. 快速排序的概念三. 快速排序的实现1. hoare2. 挖坑法3. 前后指针法 总结 正文开始 一. 前言 接上文, 前面我们了解了插入排序, 与优化版本希尔…

ssh远程管理yum源进阶

文章目录 sshNFS 共享存储服务实验yum的进阶使用Apanche做一个网页形式的源用vsftpd做一个源混合源 ssh ssh是一种安全通道协议&#xff0c;用来实现字符界面的远程登录&#xff0c;远程复制&#xff0c;远程文本传输 ssh对通信双方的数据进行了加密 用户名和密码登录 秘钥…

Mysql学习(四)——SQL通用语法之DQL

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 DQLDQL-语法基本查询条件查询聚合函数分组查询排序查询分页查询 DQL DQL数据查询语言&#xff0c;用来查询数据库中表的记录。 DQL-语法 select 字段列表 from 表…

C++ | Leetcode C++题解之第136题只出现一次的数字

题目&#xff1a; 题解&#xff1a; class Solution { public:int singleNumber(vector<int>& nums) {int ret 0;for (auto e: nums) ret ^ e;return ret;} };

图相似度j计算——SimGNN

图相似性——SimGNN 论文链接&#xff1a;个人理解&#xff1a;数据处理: feature_1 [[1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], # "A"[0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0], # "B"[0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0] # "C" 第二个循环&#xff…

Keil MDK Armcc6 总是全编译项目的问题

我碰到的问题是因为使用lib库待代替原本的源码引起的&#xff0c;把lib库去除&#xff0c;使用源码编译就不会出现全编译的问题了。但是至于一定要使用LIB库但是又不想全编译暂时不知道怎么弄&#xff0c;具体为什么会这样暂不清楚。但是可以确定的是编译器参数可能选的不对&am…

代理结算不再繁琐,Xinstall让App推广更轻松

在移动互联网时代&#xff0c;App的推广与获客已成为企业发展的重要一环。然而&#xff0c;随着推广模式的多样化&#xff0c;如何高效地管理App推广的代理结算&#xff0c;成为了许多企业面临的难题。Xinstall凭借其强大的超级渠道功能&#xff0c;为企业提供了一个完美的解决…

php实现抖音小程序支付

开发者发起下单_小程序_抖音开放平台 第一步、抖音小程序发起支付 tt.pay_小程序_抖音开放平台 前端提交订单数据到后端接口&#xff0c;然后使用 tt.pay发起支付 请求参数 属性 类型 必填 说明 order_id string 是 担保交易服务端订单号 order_token string 是 …

C语言 | Leetcode C语言题解之第136题只出现一次的数字

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<int> singleNumbers(vector<int>& nums) {int eor 0;for (int num:nums)eor ^ num;int rightOne eor & (~eor 1); // 提取出最右的1int onlyOne 0;for (int cur : nums) {if ((cur…

图像算法---自动对焦AF

一&#xff0c;CDAF反差对焦原理 CDAF&#xff0c;全称Contrast Detection Auto Focus&#xff0c;即反差式对焦或对比度检测自动对焦&#xff0c;是一种广泛应用于入门级数码相机和相机模块化智能手机上的自动对焦技术。以下是关于CDAF反差对焦的详细介绍&#xff1a; 工作原…

nginx动静分离和反向代理

一、动静分离 动静分离指的是将动态内容和静态内容分开处理。动态内容通常由后端应用程序生成&#xff0c;例如PHP、Python或Node.js&#xff0c;静态内容则包括图片、CSS、JavaScript等文件。 例子&#xff1a; #代理服务器一 server{listen 80;server_name www.dj.com;r…

C语言 | Leetcode C语言题解之第135题分发糖果

题目&#xff1a; 题解&#xff1a; int candy(int* ratings, int ratingsSize) {int ret 1;int inc 1, dec 0, pre 1;for (int i 1; i < ratingsSize; i) {if (ratings[i] > ratings[i - 1]) {dec 0;pre ratings[i] ratings[i - 1] ? 1 : pre 1;ret pre;inc…

ceph对象储存的使用

radosgw-admin user create --uid“user1” --display-name“user1” #创建用户 sudo apt install s3cmd cephadminceph-mgr01:~/ceph-cluster/s3$ s3cmd --configure Enter new values or accept defaults in brackets with Enter. Refer to user manual for detailed desc…

YUM安装httpd实验配置apache

实验目的及实验要求&#xff1a; 实验目的&#xff1a; 2.实验要求&#xff1a; &#xff08;1&#xff09;完成命令的编写&#xff0c;并能正确运行&#xff1b; &#xff08;2&#xff09;从中熟练掌握命令的功能及作用。 实验设备及软件&#xff1a; pc机 配置好Lin…

我们设计制造MW级水冷负载电阻器-数据中心船舶岸电发电机组测试大功率负载RLC阻感容集装箱负载

UEPR系列电阻采用先进材料制造&#xff0c;采用专利设计&#xff0c;将电阻与冷却液完全隔离&#xff0c;为水冷应用提供重量轻、体积小、超大功率的解决方案。其革命性的模块化设计意味着它们可以串联在一起&#xff0c;以满足您的电力需求。应用于发电、电力传输、电气传动等…

2024年汉字小达人活动还有4个多月开赛:来做18道历年选择题备考

根据近年的安排&#xff0c;2024年第11届汉字小达人比赛还有4个多月就启动&#xff0c;那么孩子们如何利用这段时间有条不紊地备考呢&#xff1f;我的建议是两手准备&#xff1a;①把小学1-5年级的语文课本上的知识点熟悉&#xff0c;重点是字、词、成语、古诗。②把历年真题刷…