【多线程中的线程安全问题】线程互斥

news2025/1/21 12:08:48

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

先来看看一些基本概念:

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

互斥量mutex:

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

比如一个大家熟知的栗子:售票。我们用一个全局整形变量记录票的个数,多个线程并发的去抢票,我们不难写出下面这样的代码:

int g_ticket=10000;

void* Run(void* args)
{
    string name=static_cast<const char*>(args);

    while(true)
    {
        if(g_ticket<=0)
        {
             break;
        }
        else
        {
            cout<<"I am "<<name<<",is running  tickets"<<g_ticket<<endl;
            g_ticket--;
        }
        usleep(2000);
    }
    return nullptr;
}


int main()
{
    pthread_t ptids[5];
    for(int i=0;i<5;++i)
    {
        char* name=new char[26];
        snprintf(name,26,"pthread%d",i+1);

        pthread_create(ptids+i,nullptr,Run,name);
    }

    for(int i=0;i<5;++i)
    {
        pthread_join(ptids[i],nullptr);
    }

    return 0;
}

当我们运行时:
在这里插入图片描述
我们发现,有多个线程抢到了同一张票,并且打印混乱。有些情况下票还有可能变成了负数,而这就是线程不安全所带来的问题,解决办法我们在下面会给出详细解释。


2 🍑用互斥锁解决线程安全问题🍑

2.1 🍎分析问题 🍎

我们来分析下上面的代码为什么会出现那样的结果?

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • 减减ticket 操作本身就不是一个原子操作。

我们可以取出渐渐ticket取出ticket–部分的汇编代码:

objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

- - 操作并不是原子操作,而是对应三条汇编指令:

  • load :将共享变量ticket从内存加载到寄存器中;
  • update : 更新寄存器里面的值,执行-1操作;
  • store :将新值,从寄存器写回共享变量ticket的内存地址。

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

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

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

2.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

这两种方式选择哪一种都是OK的。

🍋销毁互斥量🍋

销毁互斥量需要注意:

  • 使用 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_mutex_lock 时,可能会遇到以下情况:

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

所以我们可以改进下上面的抢票:

int g_tictet=10000;
pthread_mutex_t mtu=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
void* Run(void* args)
{
    string name=static_cast<const char*>(args);

    while(true)
    {
        pthread_mutex_lock(&mtu);
        if(g_tictet<=0)
        {
            pthread_mutex_unlock(&mtu);
            break;
        }
        else
        {
            cout<<"I am "<<name<<",is running  tickets"<<g_tictet<<endl;
            g_tictet--;
        }
        pthread_mutex_unlock(&mtu);
        usleep(2000);
    }
    return nullptr;
}


int main()
{
    pthread_t ptids[5];
    for(int i=0;i<5;++i)
    {
        char* name=new char[26];
        snprintf(name,26,"pthread%d",i+1);

        pthread_create(ptids+i,nullptr,Run,name);
    }

    for(int i=0;i<5;++i)
    {
        pthread_join(ptids[i],nullptr);
    }

    return 0;
}

当我们再次运行时:
在这里插入图片描述
我们发现不会出现多个线程抢占同一张票并且打印混乱的情况了。

代码中值得注意的事情有:加锁的策略是:选用的粒度一般是越细越好

🍋互斥量实现原理探究🍋

搞了这么多,那么互斥量的实现原理究竟是啥捏?

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

我们可以自己实现一份lock和unlock的伪代码

lock:
	movb $0,%al
	xchgb %al,mutex
	if(al寄存器的内容>0)
		return 0;//表示申请锁成功
	else
		挂起等待;
	goto lock;

unlock:
	movb $1,%al
	唤醒等待mutex的线程;
	return 0;//表示释放锁成功
	

通过上面的伪代码我们可以知道当初始值mutex的值为1时,假设线程1先进行申请锁,会先将寄存器中的值改为0,然后用寄存器中的0交换mutex中的1,此时1被线程1给拿到了,假设此时线程1的时间片到了,要切换线程2执行,在切换之前先保存了线程1的上下文数据,然后切换;此时线程2从头执行将寄存器中的数值改为0,然后交换,但是唯一的1已经被线程1给拿走了,所以线程而只有挂起等待;当重新切换回线程1的时候,线程1会重新恢复上下文数据,也就是寄存器的内容会被恢复到切换前,所以判断寄存器的内容>0,申请成功。此时我们发现就算是有多个线程并发的抢占锁资源时,也只有一个线程能够申请成功,其他线程在挂起等待,因为这里面的1只有一个,并且是以交换形式进行的,可以理解这里面的1本质就是一把锁。

释放资源就更好理解了,将寄存器的值修改为1,然后唤醒等待锁的线程即可。从释放锁的那段伪代码中我们也能够看到:当多个线程申请同一把锁时,一个线程申请了锁后,虽然其他线程不能够申请了,但是却可以释放该锁


2.3 🍎可重入VS线程安全 🍎

🍋概念🍋

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

🍋常见的线程不安全/安全的情况🍋

不安全情况:

  • 1️⃣不保护共享变量的函数
  • 2️⃣函数状态随着被调用,状态发生变化的函数
  • 3️⃣返回指向静态变量指针的函数
  • 4️⃣调用线程不安全函数的函数

安全情况:

  • 1️⃣每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 2️⃣类或者接口对于线程来说都是原子操作
  • 3️⃣多个线程之间的切换不会导致该接口的执行结果存在二义性

🍋常见不可重入/可重入的情况🍋

不可重入:

  • 1️⃣调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 2️⃣调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 3️⃣可重入函数体内使用了静态的数据结构

可重入:

  • 1️⃣不使用全局变量或静态变量
  • 2️⃣不使用用malloc或者new开辟出的空间
  • 3️⃣不调用不可重入函数
  • 4️⃣不返回静态或全局数据,所有数据都有函数的调用者提供
  • 5️⃣使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

🍋可重入与线程安全联系与区别🍋

联系:

  • 1️⃣函数是可重入的,那就是线程安全的
  • 2️⃣函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 3️⃣如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

区别:

  • 1️⃣可重入函数是线程安全函数的一种
  • 2️⃣线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 3️⃣如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

2.4 🍎死锁🍎

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

🍋死锁四个必要条件🍋

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

🍋避免死锁🍋

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

死锁避免算法有银行家算法和死锁检测算法,大家有兴趣可以自行下去研究。


3 🍑用封装使代码更加优雅 🍑

我们上面写的代码中,我们能否自己实现一个简易版本的创建线程(类似于C++11提供的线程库那样)的类呢?以及加锁和解锁能够使用RAII的思想来帮助我们完成呢?当然是可以的,我们可以自己实现一个更加优雅的代码:

mutexGuard.hpp:

#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;

class mutexGurad
{
public:
    mutexGurad(pthread_mutex_t* mutex)
    :_mutex(mutex)
    {
        pthread_mutex_lock(_mutex);
    }
    ~mutexGurad()
    {
        pthread_mutex_unlock(_mutex);
    }

private:
    pthread_mutex_t* _mutex;
};

thread.hpp:

#pragma once
#include <iostream>
#include <functional>
using namespace std;

class threadProcess
{
public:
    enum stu
    {
        NEW,
        RUNNING,
        EXIT
    };
    template <class T>
    threadProcess(int num, T exe, void *args)
        : _tid(0),
          _status(NEW),
          _exe(exe),
          _args(args)
    {
        char name[26];
        snprintf(name, 26, "thread%d", num);
        _name = name;
    }

    static void *runHelper(void *args)
    {
        threadProcess *ts = (threadProcess *)args; 
        
        (*ts)();
        return nullptr;
    }

    void operator()() // 仿函数
    {
        if (_exe != nullptr)
            _exe(_args);
    }

    void Run()
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);
        if (n != 0)
            exit(-1);
        _status = RUNNING;
    }

    void Join()
    {
        int n = pthread_join(_tid, nullptr);
        if (n != 0)
            exit(-1);
        _status = EXIT;
    }


private:
    string _name;
    pthread_t _tid;
    stu _status;
    function<void *(void *)> _exe;
    void *_args;
};

测试程序:

int g_tictet = 10000;
pthread_mutex_t mtu = PTHREAD_MUTEX_INITIALIZER;

void *Run(void *args)
{
    string name = static_cast<const char *>(args);

    while (true)
    {
        {
            mutexGurad mutGuard(&mtu);
            if (g_tictet <= 0)
            {
                break;
            }
            else
            {
                cout << "I am " << name << ",is running  tickets" << g_tictet << endl;
                g_tictet--;
            }
        }

        usleep(1000);
    }

    return nullptr;
}

int main()
{
    threadProcess thpro1(1, Run, (void *)"thread1");
    threadProcess thpro2(2, Run, (void *)"thread2");
    threadProcess thpro3(3, Run, (void *)"thread3");
    thpro1.Run();
    thpro2.Run();
    thpro3.Run();

    thpro1.Join();
    thpro2.Join();
    thpro3.Join();
    return 0;
}

当我们运行时:
在这里插入图片描述
我们依旧能够得到正确的结果,并且代码写起来也好看多了。除此之外,我们还可以拿到线程的其他特性,这里我就不在测试了。

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

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

相关文章

python与深度学习(十一):CNN和猫狗大战

目录 1. 说明2. 猫狗大战2.1 导入相关库2.2 建立模型2.3 模型编译2.4 数据生成器2.5 模型训练2.6 模型保存2.7 模型训练结果的可视化 3. 猫狗大战的CNN模型可视化结果图4. 完整代码5. 猫狗大战的迁移学习 1. 说明 本篇文章是CNN的另外一个例子&#xff0c;猫狗大战&#xff0c…

建立动态数组,输入5个学生的,另外用一个函数检查其中有无低于60分的,输出不合格的成绩。

题为c程序设计&#xff08;第五版&#xff09;谭浩强 例8.30 文章目录 前言一、pandas是什么&#xff1f;二、使用步骤 1.引入库2.读入数据总结 前言 这篇博客&#xff0c;让我们一起来学习内存的动态分配。 那么&#xff0c;什么是内存的动态分配呢&#xff1f;C语言允许建立…

RS485或RS232转ETHERCAT连接ethercat转换器

最近&#xff0c;生产管理设备中经常会遇到两种协议不相同的情况&#xff0c;这严重阻碍了设备之间的通讯&#xff0c;串口设备的数据不能直接传输给ETHERCAT。这可怎么办呢&#xff1f; 别担心&#xff0c;捷米JM-ECT-RS485/232来了&#xff01;这是一款自主研发的ETHERCAT从站…

FreeRTOS源码分析-7 消息队列

目录 1 消息队列的概念和作用 2 应用 2.1功能需求 2.2接口函数API 2.3 功能实现 3 消息队列源码分析 3.1消息队列控制块 3.2消息队列创建 3.3消息队列删除 3.4消息队列在任务中发送 3.5消息队列在中断中发送 3.6消息队列在任务中接收 3.7消息队列在中断中接收 1 消…

【RTT驱动框架分析03】- sfus flash 操作库的分析和基于STM32F103RCT6+CUBEMX的SFUS移植教程

sfus flash 操作库的分析 sfus 抽象 /*** serial flash device*/ typedef struct {char *name; /**< serial flash name */size_t index; /**< index of flash device information table see flash_…

filter shape、padding、strides三者之间的关系

filter shape 在深度学习中&#xff0c;“filter shape”&#xff08;滤波器形状&#xff09;指的是卷积神经网络中滤波器&#xff08;也称为卷积核&#xff09;的维度或大小。滤波器是用于在卷积层中提取特征的重要组件。 滤波器形状通常是一个四维张量&#xff0c;具体取决…

小研究 - 一种复杂微服务系统异常行为分析与定位算法(二)

针对极端学生化偏差&#xff08;&#xff25;&#xff58;&#xff54;&#xff52;&#xff45;&#xff4d;&#xff45; &#xff33;&#xff54;&#xff55;&#xff44;&#xff45;&#xff4e;&#xff54;&#xff49;&#xff5a;&#xff45;&#xff44; &#…

并发编程 - CompletableFuture

文章目录 Pre概述FutureFuture的缺陷类继承关系功能概述API提交任务的相关API结果转换的相关APIthenApplyhandlethenRunthenAcceptthenAcceptBoththenCombinethenCompose 回调方法的相关API异常处理的相关API获取结果的相关API DEMO实战注意事项 Pre 每日一博 - Java 异步编程…

DPN(Dual Path Network)网络结构详解

论文&#xff1a;Dual Path Networks 论文链接&#xff1a;https://arxiv.org/abs/1707.01629 代码&#xff1a;https://github.com/cypw/DPNs MXNet框架下可训练模型的DPN代码&#xff1a;https://github.com/miraclewkf/DPN 我们知道ResNet&#xff0c;ResNeXt&#xff0c;D…

面向对象【对象数组的使用与内存分析、方法重载、可变个数形参】

文章目录 对象数组实例对象内存分析 方法的重载重载方法调用打印方法的重载 可变个数形参的方法特点传递任意数量的参数与其他参数共存传递数组或多个参数 对象数组 存储对象引用的数组。它允许您在单个数组中存储多个对象&#xff0c;并通过索引访问和操作这些对象。 实例 创…

使用SpringBoot+SpringMVC+Mybatis+Redis实现个人博客管理平台

文章目录 前言1. 项目概述2. 项目需求2.1功能需求2.2 其他需求2.3 系统功能模块图 3. 开发环境4. 项目结构5. 部分功能介绍5.1 数据库密码密文存储5.2 统一数据格式返回5.3 登录拦截器 6. 项目展示 前言 在几个月前实现了一个servlet版本的博客系统&#xff0c;本项目则是在原…

JWT无状态理解

JSON Web Tokens (JWT) 被称为无状态&#xff0c;因为授权服务器不需要维护任何状态&#xff1b;令牌本身就是验证令牌持有者授权所需的全部内容。 JWTs都签订使用数字签名算法&#xff08;例如RSA&#xff09;不能被伪造。因此&#xff0c;任何信任签名者证书的人都可以放心地…

二维深度卷积网络模型下的轴承故障诊断

1.数据集 使用凯斯西储大学轴承数据集&#xff0c;一共有4种负载下采集的数据&#xff0c;每种负载下有10种 故障状态&#xff1a;三种不同尺寸下的内圈故障、三种不同尺寸下的外圈故障、三种不同尺寸下的滚动体故障和一种正常状态 2.模型&#xff08;二维CNN&#xff09; 使…

基于传统检测算法hog+svm实现图像多分类

直接上效果图&#xff1a; 代码仓库和视频演示b站视频005期&#xff1a; 到此一游7758258的个人空间-到此一游7758258个人主页-哔哩哔哩视频 代码展示&#xff1a; 数据集在datasets文件夹下 运行01train.py即可训练 训练结束后会保存模型在本地 运行02pyqt.py会有一个可视化…

iOS开发-自定义TabbarController与Tabbar按钮Badge角标

iOS开发-自定义Tabbar按钮Badge角标 Tabbar是放在APP底部的控件。UITabbarController是一个非常常见的一种展示模式了。比如微信、QQ都是使用tabbar来进行功能分类的管理。 一、实现自定义Tabbar 我这里Tabbar继承于系统的UITabBar&#xff0c;定义背景图、线条的颜色、tab…

Redis系列 2- Redis 的持久化

Redis系列 2- Redis 的持久化 1、关于 Redis 持久化2、RDB 持久化2.1、RDB 文件的创建与载入2.2、RDB 自动间隔性保存的配置2.3、Redis 快照2.4、RDB 重点总结 3、AOF 持久化3.1、命令追加(append)3.2、AOF 文件的写入与同步3.3、AOF 工作原理3.4、AOF 的文件载入与数据还原3.5…

QGIS3.28的二次开发一:编译工程

环境&#xff1a;VS2019OSGeo4WCMake_3.26Cygwin64QGIS_3.28 注意&#xff1a;一定要按照步骤顺序来&#xff01; 一、配置环境 &#xff08;一&#xff09;VS2019 VS2019下载链接https://my.visualstudio.com/Downloads?qvisual%20studio%202019&wt.mc_ido~msft~vsco…

day50-Insect Catch Game(捉虫游戏)

50 天学习 50 个项目 - HTMLCSS and JavaScript day50-Insect Catch Game&#xff08;捉虫游戏&#xff09; 效果 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport"…

基于SpringBoot+Vue的大学生租房系统设计与实现(源码+LW+部署文档等)

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

第 356 场力扣周赛题解

A 满足目标工作时长的员工数目 签到题 class Solution { public:int numberOfEmployeesWhoMetTarget(vector<int> &hours, int target) {int res 0;for (auto x: hours)if (x > target)res;return res;} };B 统计完全子数组的数目 枚举子数组&#xff1a;枚举子数…