【Linux】线程安全篇Ⅰ

2023-05-16

文章目录

  • 0、概述
  • 1、线程不安全举例
    • 1.1 前提知识铺垫
    • 1.2 场景模拟
    • 1.3 代码模拟
  • 2、互斥
    • 2.1 什么是互斥
    • 2.2 互斥锁的原理&&特性
    • 2.3 互斥锁的计数器如何保证原子性
    • 2.4 互斥锁的接口
      • 2.4.1 初始化接口
      • 2.4.2 加锁接口
      • 2.4.3 解锁接口
      • 2.4.4 销毁接口

0、概述

本文主要介绍了以下几点内容:
1、线程存在不安全(程序结果二义性)的情况
2、线程的不安全可以通过互斥的思想,利用互斥锁来解决
3、如何使用互斥锁的相关接口&&注意事项

1、线程不安全举例

1.1 前提知识铺垫

1、线程在Linux操作系统中也是用一个task_struct结构体来进行描述的。

2、多个线程之间也是抢占式执行的。对于多核CPU来说,特们可以并行执行,对于单核CPU来说,只能是并发执行。

3、与进程切换一样,每个线程在进行切换的时候,它对应的PCB中也会有程序计数器和上下文信息来保存该线程的执行状态,确保下一次被调用之后能够正确执行

4、原子性:某一过程只会存在两种状态,要么全部执行完毕,要么还没有开始执行。不存在中间状态。

1.2 场景模拟

有一个单核的CPU,有两个线程,分别是线程A和线程B,有一个全局的整型变量num,初始值为10;

线程A 和线程B 执行的代码都是对全局变量进行+1操作。

由于线程A和B是抢占式执行的,那么一定会有以下这种情况的存在:

①我们假设A先拿到CPU资源将num的值从内存读取到寄存器中,此时由于某些原因(CPU调度),线程A被剥离CPU。此时线程A的PCB中的程序计数器和上下文信息记录了线程A的执行状态以及下一条要执行的指令

②然后由线程B拿到CPU资源,将num的值从内存中读取到寄存器,通过CPU计算后回写到寄存器,然后再回写到内存当中。此时内存中num的值为11

③此时线程B的代码执行完毕,线程A重新获得CPU资源。此时线程A并不会从内存中读取num的值,而是通过程序计数器和上下文信息来恢复现场因此线程A 拿到的数据依旧是最开始的10,对其自增后,回写到寄存器,再回写到内存中。此时内存中的num值依旧是11

④此时,统观整个过程来看,线程A和线程B分别对全局变量num进行了+1操作,理应num的值应该为12,但是现在的结果却是11。这就是线程不安全的具体表现,程序的结果产生了二义性。

下面我们通过图解进一步理解上述的过程:
在这里插入图片描述
线程不安全的原因本质上就是对临界区的非原子性访问导致的

1.3 代码模拟

下面我们通过代码来模拟以下上面的场景。
在这里插入图片描述
在这里插入图片描述
注意:这并不是一个必现的结果,也就是说结果有可能正确,有可能错误。并不确定。在单核CPU环境下测试比比较困难,现象不是很明显。但是,线程不安全是一定存在的。也是必须要解决的!

2、互斥

2.1 什么是互斥

互斥是一种控制线程访问时序的手段。具体如下:
在这里插入图片描述

2.2 互斥锁的原理&&特性

1、原理
互斥锁本质上是一个0/1计数器,计数器的取值只能是0或1
 计数器值为1:表示当前线程可以获取到互斥锁,从而去访问临界资源
 计数器值为0:表示当前线程不能获取到互斥锁,也就不能访问临界资源
2、特性
在这里插入图片描述

2.3 互斥锁的计数器如何保证原子性

为什么计数器当中的值从0变为1,或者从1变为0是原子性的呢?
计数器中值的变换是直接使用寄存器当中的值和计数器内存的值进行交换。该交换过程使用一条汇编指令就可以完成,因此是原子性的。
具体过程如下:

加锁过程:将寄存器中的值设置为0
 情况1:计数器的值为1,说明锁空闲,没有被线程加锁
在这里插入图片描述
 情况2:计数器的值为0,说明锁忙碌,被其他线程加锁拿走
在这里插入图片描述
交换结束后,判断寄存器中的值,如果值为1,表示加锁成功,如果值为0,表示加锁失败。

解锁过程:

直接使用交换的汇编指令将1和计数器内存中的值进行交换

保证互斥锁计数器原子性的汇编伪码如下:

lock:
       movb $0,%al
        xchgb %al,mutex     交换指令
        if(al 寄存器的内容 > 0){
              return 0;    
         }
        else{
              挂起等待
          }
        goto lock;
unlock:
           movb $1, mutex
            唤醒等待Mutex的线程;
             return 0

2.4 互斥锁的接口

2.4.1 初始化接口

动态初始化
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
参数

mutex:传递互斥锁对象
attr:互斥锁属性,一般传递为NULL,表示使用默认属性

返回值

初始化成功,返回0
初始化失败,设置errno,并将errno返回

静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
#define PTHREAD_MUTEX_INITIALIZER {{0,0,0,0,0…}}结构体的初始化

2.4.2 加锁接口

int pthread_mutex_lock(pthread_mutex_t *mutex);
参数

mutex:传递互斥锁对象

返回值

加锁成功,返回0
在这里插入图片描述

int pthread_mutex_trylock(pthread_mutex_t *mutex);
参数

mutex:传递互斥锁对象

返回值

加锁成功,返回0
在这里插入图片描述

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout)

是一种带有超时时间的加锁接口,具体细节见下图:

在这里插入图片描述

🆗,现在我们通过代码验证一下初始化和加锁的接口。
我们发现上面的代码存在结果的二义性,是一种不安全的现象,因此我们需要通过加锁的方式保证对临界区代码访问的原子性,也就是保证了线程的安全。
demo:

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

int g_ticket = 10000;
pthread_mutex_t g_lock;//互斥锁

void* thread_start(void* arg)
{
    //加锁
    pthread_mutex_lock(&g_lock);
    //修改全局变量
    while(g_ticket > 0)
    {
        printf("I am %p,I got value is %d\n",pthread_self(),g_ticket);
        g_ticket--;
    }
}


int main()
{
    //初始化互斥锁
    pthread_mutex_init(&g_lock,NULL);
    pthread_t tid[2];
    //创建两个线程A和B
    for(int i= 0;i<2;++i)
    {
        int ret = pthread_create(&tid[i],NULL,thread_start,NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }
    }

    //主线程等待接收
    for(int i=0;i<2;i++)
    {
        pthread_join(tid[i],NULL);
    }

    return 0;
}

在这里插入图片描述
分析代码运行结果
在这里插入图片描述
目前程序无法退出是因为有一个线程拿不到互斥锁,那应该如何解决呢?
让拿到锁的线程在退出的时候将互斥锁释放就好了!下面我们就来介绍以下互斥锁的解锁接口~

2.4.3 解锁接口

int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数

mutex:要解锁的互斥锁变量

返回值

解锁成功,返回0
解锁失败,设置errno,并将errno返回

好的,我们再次将代码更改如下:
在这里插入图片描述
运行结果:
在这里插入图片描述
为什么程序不会退出呢?
在这里插入图片描述
通过pstack + 进程号 查看堆栈调用情况。
在这里插入图片描述

如何解决上面地问题呢?只需要在break之前释放掉互斥锁就可以了
在这里插入图片描述
再次运行,查看结果,发现一切正常
在这里插入图片描述
综上,我们在解锁接口中得到一个重要的结论:
在线程所有有可能退出的地方都进行解锁,否则就有可能导致死锁。

2.4.4 销毁接口

pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:

mutex:想要销毁的互斥锁

注意:

如果是动态初始化互斥锁,需要调用销毁接口。
如果是静态初始化互斥锁,不需要调用销毁接口。

到这里,互斥锁相关的接口就介绍完了。在最后附上一份完整的测试代码。

#include<stdio.h>
#include<unistd.h>
#include<pthread.h>

int g_ticket = 10;
pthread_mutex_t g_lock;//互斥锁

void* thread_start(void* arg)
{
    //修改全局变量
    while(1)
    {
        sleep(1);
        //加锁
        pthread_mutex_lock(&g_lock);
        if(g_ticket <= 0)
        {
            pthread_mutex_unlock(&g_lock);
            break;
        }
        printf("I am %p,I got value is %d\n",pthread_self(),g_ticket);
        g_ticket--;
        //解锁
       pthread_mutex_unlock(&g_lock);
    }
}


int main()
{
    //初始化互斥锁
    pthread_mutex_init(&g_lock,NULL);
    pthread_t tid[2];
    //创建两个线程A和B
    for(int i= 0;i<2;++i)
    {
        int ret = pthread_create(&tid[i],NULL,thread_start,NULL);
        if(ret < 0)
        {
            perror("pthread_create");
            return 0;
        }
    }

    //主线程等待接收
    for(int i=0;i<2;i++)
    {
        pthread_join(tid[i],NULL);
    }

    //销毁互斥锁
    pthread_mutex_destroy(&g_lock);

    return 0;
}

感谢你看到文末,感觉有所收获的话,还请多多支持~
下篇谈一谈同步存在的意义~我们下篇见!
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【Linux】线程安全篇Ⅰ 的相关文章

  • ros学习之串口通信(数据读取),并进行发布

    串口参数 波特率 9600 起始位 1 数据位 8 停止位 1 奇偶校验 无 例如超声波模组地址为0X01 则主机发送 0X55 0XAA 0X01 0X01 checksum checksum 61 帧头 43 用户地址 43 指令 am
  • 在Ubuntu上使用LVM对ROOT进行在线扩容

    前提 xff1a 在安装ubuntu的时候 xff0c 是使用LVM进行分区管理的 背景 xff1a 我在安装的时候 xff0c 选择了500G大小 xff0c 磁盘总大小1T xff0c 现在想扩成1T 扩容前 xff1a yang 64
  • realsense D435i双目IMU 数据集

    realsense D435i 双目IMU数据集 使用双目 43 IMU的数据双目内参双目IMU外参 使用双目 43 IMU的数据 双目内参 model type PINHOLE camera name camera image width
  • MobaXterm 登录出现 Network error :Connection timed out

    本来用SSH连接正在操作 xff0c 突然连接不好Linux xff0c 无法登陆 xff0c 出现Network error Connection timed out错误 还以为是自己哪里操作出错了 xff0c 打开本机 cmd命令窗口
  • 消息队列总结

    一 为什么需要无锁队列 xff1f 二 无锁队列是什么 xff1f 三 无锁队列是如何实现的 xff1f span class token keyword inline span span class token class name yq
  • 姿态传感器—MPU6050

    姿态传感器 MPU6050 简介寄存器数字运动处理器 DMP遇到的问题1 初始化是要水平放置 且 按照上电时的方位为基准 xff08 正点原子提供的例程 xff09 简介 MPU6050是一款六轴 xff08 三轴加速度 43 三轴角速度
  • 卡尔曼滤波的优点总结

    卡尔曼滤波的优点不在于它的估计的偏差小多少 xff0c 而在于它巧妙的融合了观测数据与估计数据 xff0c 对误差进行闭环管理 xff0c 将误差限定在一定范围 xff0c 试想 xff0c 如果没有两者的信息融合 xff0c 只有估计数据
  • 个人简历2021

    标题 个人简历 日期 2021 09 27 23 42 57 标签 简历 分类 工作 职业发展 说下我的个人简历吧 xff0c 希望大家能够了解我 xff0c 一起在技术这条路上一直走下去 个人信息 姓名性别年龄现居地址邮箱陈作立男29上海
  • 深入理解图优化与g2o:图优化篇 - 半闲居士 - 博客园 转

    深入理解图优化与g2o xff1a 图优化篇 半闲居士 博客园
  • 二次型优化问题矩阵求导解法

    二次型求导 风之舞555 博客园 https www csdn net tags MtTaEgzsOTU2NzAxLWJsb2cO0O0O html
  • SQL2000 好书 《SQL Server 2000数据库管理与开发技术大全》----求是科技 人民邮电出版社

    SQL2000 好书 SQL Server 2000数据库管理与开发技术大全 求是科技 人民邮电出版社
  • grub启动

    grub启动 如何修复引导 现象 开机直接进入grub rescue模式 解决方案 第一步 xff1a 退出rescue模式 一般只需要设置prefix变量 span class token comment 通过ls 命令查看所有的磁盘 s
  • aruco安装 配合realsense 使用

    使用github安装 网址 xff1a http www uco es investiga grupos ava node 26 git clone到本地之后 xff0c catkin make即可开始使用 使用apt安装 span cla
  • VS连接realsense D435i摄像头(4)——使用PCL绘制点云图

    本篇主要是在使用PCL绘制点云过程中遇到的问题 xff0c 初始化参照该博客 电脑系统 xff1a win10 x64Visual Studio 2019Realsense D435i摄像头使用语言 xff1a C xff0c C 43 4
  • MobaXterm 无法显示弹框或界面

    MobaXterm 无法显示弹框或界面的解决方案之一 xff1a Settings gt Configuration gt X11 gt Xorg version xff1a 选择Mobox 1 20 4 版本越新越好 亲测可用
  • VINS_FUSION的global融合思想

    VINS FUSION的global融合思想 文章目录 VINS FUSION的global融合思想 使用全局融合的原因 GPS的缺点 融合的目的 算法架构 观测和状态约束关系 GPS 融合思路 GPS残差计算 代码段 參考文献 使用全局融
  • ROS下使用 Realsense D435i 运行ORBSLAM2_with_pointcloud_map

    首先clone高博的代码 git clone https span class token operator span span class token operator span span class token operator spa
  • windows 下构建 Eclipse gdb 调试平台调试arm程序

    xfeff xfeff 安装yargarto 版本eabi编译链 http www yagarto de 安装JAVA虚拟机 JRE 就足够了 下载 eclipse 的 C C 43 43 版本 下载 zylin 插件 xff0c 在 ec
  • 我的2013—弃金融IT,从SAP业务

    我的2013 xff0c 是动荡的一年 xff1b 这一年 xff0c 我跳巢了 xff1b 这一年 xff0c 我换行业了 xff1b 这一年 xff0c 我离开了生活5年的长春 xff0c 来到成都 xff1b 这一年 xff0c 我放
  • Int和Integer的区别

    文章目录 一 Int和Integer的区别Int和Integer的比较 xff08 扩展 xff09 一 Int和Integer的区别 1 Integer是Int的包装类 xff0c Int是八种基本数据类型之一 2 Integer变量必须

随机推荐