六、QtC++原子操作详解

C++原子操作

因为在大多时候,Qt库以C++的接口提供,所以本篇所讲虽然划分为QT栏,但是其实是C/C++进阶的必要

为什么要说原子操作呢,因为我下两篇将要深入分析多线程以及QT中的多线程,线程就必然涉及线程同步,举个例子,假如我们设计一个软件,一个线程负责采集传感器数据,一个线程负责将数据显示到屏幕,而采样的频率(速度)远远大于屏幕刷新速率,假如每秒采集10次,而屏幕上的数据1秒才更新一次,反正诸如这样的场景,都需要线程同步,换句话说,也可以理解为控制线程的运行顺序和运行速度

而线程控制,大多用到以下几种手段

  • 互斥锁
  • 信号量
  • 标志位

其实还有其他的,本篇就不逐一分析,但是大多数场景,使用都是这三种,关于互斥锁,用来保护临界区的,信号量用来控制线程执行速度不一样的场景,比如经典的生产者与消费者,本篇也不分析,我们在后面会详细讲生产者与消费者的例子,本篇讲的原子操作,主要是跟标志位有关。

我们来看下面的例子

extern bool myflag;
virtual void run()
{
	while(true)
    {
        if(myflag)
        {
            //此处是我们要执行的代码
        }
        else
        {
            sleep(5);//延时5毫秒,挂起此线程,让CPU调度其他线程
        }
    }
}


这样的代码大家一定很熟悉,在C++中,继承Thread,重写virtual void run() ,然后启动这个线程开始运行,而我们控制这个线程是否执行,可以通过一个全局变量extern bool myflag;来进行,当想要线程运行是,我们让这个变量为true,当不让线程运行时,我们让这个变量为false。这个场景大家都很熟悉,但是,下面给大家一个灵魂拷问:

我们知道,当涉及多线程访问全局变量的时候,理论上涉及多线程访问同一块内存,涉及写的情况,都需要使用互斥锁或者信号量来进行同步或者互斥操作,这个学过操作系统原理,稍微有一点基础的同学基本都是知道的,但是

**这个extern bool myflag明明没有加互斥锁等,为什么不会出错,为什么能这样使用,或者说,或者有一种情况,互斥锁大多时候也是全局的,互斥锁本身是如何互斥的?**

因为这是原子操作,原子操作在任何情况下包括多线程下,原子操作是不需要写保护的,为什么呢?

我们来区分几个概念

  1. CPU位数

在这里插入图片描述

说直白一些就是CPU寄存器的位宽,也就是大小,我们的台式机X86架构的这种,没有研究过,但是比如熟悉的STM32,GD32等等ARM架构的MCU,IMX6U等等,内部有R0~R12(通用寄存器),R13(堆栈寄存器)、R14(链接寄存器)、以及R15(PC指针)等等,这些寄存器是32位的

  1. 总线位宽
    在这里插入图片描述

    CPU是通过总线和内存以及其他的一些IO设备进行连接的,然后我们的程序在内存中,数据也在内存中,程序运行的时候,CPU就会去读取程序也就是指令,然后执行

    我们来看指令是什么

    指令 = 操作码+操作数

    • 操作码:就是加减乘除,取地,读寄存器,读内存等等,与或等等,也就是CPU支持的操作

    • 操作数:就是数据,可能是立即数或者寄存器

      void test(void)//这是一个简单的C程序,我们来试着简单的翻译成汇编
      {
      00000000	int a = 10;
      00000004	int b = 20;
      00000008	int c;
      0000000c	c = a+b;
      }
      
      test:
      	mov r0 , #00000000 //a的地址先加载到R0,指针就是地址,一般都是4字节,这就是指针本质
          ldr r1 , [r0]		 //将r0中存的地址中的数据加载到R1	
              
          mov r0 , #00000004 //b的地址先加载到R0
          ldr r2 , [r0]		 //将r0中存的地址中的数据加载到R2
              
          add r3 , r1, r2		//将运算结果存入r3
              
          mov r0 , #00000008 //将变量c的地址加载入r0
              
          str	 r3, [r0]      //将运算结果写入r0中存的地址中
      
      

    上面的汇编不一定准确,经过编译器编译和优化,通过一些复杂的指令,可能会非常精简,上面是手动翻译的程序执行过程,所以相对繁琐,那这跟原子操作有什么关系呢?有

    原子操作,就是多线程相互抢占操作一个变量,都不会产生影响的,都叫原子操作,首先操作的数据不能超过32位,,因为超过32位的话,CPU是不能一次性加载完的

    比如假如有一个全局变量extern int a,当运行这段程序,对这段程序的变量进行入栈的时候,往往每个变量都会变成一个编号,这个变量全局只有一份,地址是固定的,当执行代码a = 5时,会变成如下指令

    mov r0 , #a的地址

    ldr [r0] , #0x05

    我们分析发现,线程切换的时候,都不会影响当前线程的结果,为什么呢?

    • 假如CPU在运行这两条指令的时候,没有发生线程调度,或者说没有被抢占,那就成功执行,结果确定
    • 假如执行了第一条,发生了线程调度,而在其他线程中又改写了a的值,但是但是但是,当线程切换回来的时候,当前线程的寄存器会被全部恢复成原样,也就是调度之前的,然后继续执行第二条指令,结果还是写入了0x05,因为线程切换会保存上现场上下文,其实就是保存这些寄存器,将这些寄存器的值写入当前线程所在的内存,也就是线程栈中,也可以称为将寄存器压栈,当线程切换回来的时候,又弹栈恢复寄存器,而PC指针,则记录了当前执行到那一条指令,或者说那一条代码

    a = a+5;a++;则不能称之为原子操作,因为

    假如初始值a = 10;

    在当前线程中,有a = a+5;按照正确的话,执行完的瞬间,a 应当等于15 ,

    mov r0 , #a的地址

    ldr r1 , [r0]

    add r2 , r1 , #0x05

    str [r0] , r2

    注意,上面的四条汇编是一条C代码哦,也就是a+=5;如果在第一条指令完之后,发生了线程调度,而其他线程改变了a的值等于50,当线程调度回来继续执行第二条指令时,就会将50写入寄存器,这条代码执行完之后,结果就成了55 , 结果就错了

  • 其实我写的汇编可能不是很准确,我猜测,像a = 5,这样的指令,有可能经过编译器编译之后,会直接变成

    LDR R1 , #0x05,有可能一些比较简单的指令,我们想着不是原子操作,而经过编译器优化,会变成原子操作,编译器总是希望我们更少的出错嘛,这就不深入研究编译器了

分析这么多,原子操作的使用其实很简单,如果是一次性写入以内的32字节的数据,就是原子操作,而需要先读,再写入的,就不是原子操作。所以这就是为什么我们可以通过操作全局标志位,来控制线程的原因

更为常见的场景是,多个线程操作同一个链表啊,队列,数组这样,这个时候涉及访问这块内存的代码加互斥锁,其实就是将非原子操作变为原子操作,理论上互斥锁就是这样的功能,将非原子操作变为伪原子操作。

以上就是关于原子操作的深入详解,手动肝真的好费时间,有想过出录制视频可能会更直观和清晰,以后再说吧

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值