整型变量的原子操作

Posted vector6_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了整型变量的原子操作相关的知识,希望对你有一定的参考价值。

为什么整型变量赋值操作不是原子的

常见的整型变量操作有如下几种情况:

  • 给整型变量赋值一个确定的值,如

    int a = 1;
    

    这条指令操作一般是原子的,因为对应着一条计算机指令,CPU 将立即数 1 搬运到变量 a 的内存地址中即可,汇编指令如下:

    mov dword ptr [a], 2
    

    然而这确是最不常见的情形,由于现代编译器一般存在优化策略,如果变量 a 的值在编译期间就可以计算出来(例如这里的例子中 a 的值就是1),那么 a 这个变量本身在正式版本的软件中(release版)就很有可能被编译器优化掉,凡是使用 a 的地方,直接使用常量 1 来代替。所以实际的执行指令中,这样的指令存在的可能性比较低。

  • 变量自增或者自减一个值,如

    a ++;
    

    从 C/C++ 语法的级别来看,这一条语句是原子的;但是从实际执行的二进制指令来看变量的增减涉及到内存值向寄存器搬移、增寄存器减及计算结果搬移回内存等操作,也不是原子的。

    具体来说,其一般对应三条指令,

    a)首先将变量 a 对应的内存值搬运到某个寄存器(如 eax )中,b)然后将该寄存器中的值自增 1,c)再将该寄存器中的值搬运回 a 代表的内存中:

    mov eax, dword ptr [a]  
    inc eax
    mov dword ptr [a], eax
    

    另一个例子,现在假设 a 的值是0,有两个线程,每个线程对变量 a 的值递增 1,我们预想的结果应该是 2,可实际运行的结果可能是 1!分析如下:

    int a = 0;
    //线程1
    void thread_func1()
    {
        a++;
    }
    //线程2
    void thread_func2()
    {
        a++;
    }
    

在这里插入图片描述

我们预想的结果是线程 1线程 2 的三条指令各自执行,最终 a 的值变为 2,但是由于操作系统线程调度的不确定性,线程 1 执行完指令①和②后,eax 寄存器中的值变为 1,此时操作系统切换到 线程2 执行,执行指令③④⑤,此时 eax 的值变为1;接着操作系统切回线程 1 继续执行,执行指令⑥,得到 a 的最终结果 1

  • 把一个变量的值赋值给另外一个变量,或者把一个表达式的值赋值给另外一个变量,如:

    int a = b;
    

    从 C/C++ 语法的级别来看,这是也是一条语句,是原子的;但是从实际执行的二进制指令来看,由于现代计算机CPU架构体系的限制,数据不可以直接从内存搬运到另外一块内存,必须借助寄存器中断;这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄存器(如eax)中,再从该寄存器搬运到变量a的内存地址:

    mov eax, dword ptr [b]  
    mov dword ptr [a], eax
    

    多个线程在执行这两条指令时,和上一种情况类似,也可能某个线程可能会在第一条指令执行完毕后被剥夺 CPU时间片,切换到另外一个线程而产生不确定的情况。

C++11 对整型变量原子操作的支持

在 C++ 98/03 标准中,如果想对整型变量进行原子操作,要么利用操作系统提供的相关原子操作 API,要么利用对应操作系统提供的锁对象来对变量进行保护,无论是哪种方式,编写的代码都无法实现跨平台操作,而C++ 11新标准中提供了对整型变量原子操作的相关库,即 std::atomic ,这是一个模板类型:

template<class T>
struct atomic;

你可以传入具体的整型类型(如bool、char、short、int、uint等)对模板进行实例化,实际上 stl 库也提供了这些实例化的模板类型:

类型别名定义
std::atomic_boolstd::atomic
std::atomic_charstd::atomic
std::atomic_scharstd::atomic
std::atomic_ucharstd::atomic
std::atomic_shortstd::atomic
std::atomic_ushortstd::atomic
std::atomic_intstd::atomic
std::atomic_uintstd::atomic
std::atomic_longstd::atomic
std::atomic_ulongstd::atomic
std::atomic_llongstd::atomic
std::atomic_ullongstd::atomic
std::atomic_char16_tstd::atomic<char16_t>
std::atomic_char32_tstd::atomic<char32_t>
std::atomic_wchar_tstd::atomic<wchar_t>
std::atomic_int8_tstd::atomic<std::int8_t>
std::atomic_uint8_tstd::atomic<std::uint8_t>
std::atomic_int16_tstd::atomic< std::int16_t>
std::atomic_uint16_tstd::atomic<std::uint16_t>
std::atomic_int32_tstd::atomic<std::int32_t>
std::atomic_uint32_tstd::atomic<std::uint32_t>
std::atomic_int64_tstd::atomic<std::int64_t>
std::atomic_uint64_tstd::atomic<std::uint64_t>

在c++语言对原子变量支持以后,我们就可以很方便的使用可以跨平台的原子操作了,

#include <atomic>
#include <stdio.h>

int main()
{
    std::atomic<int> value;
    value = 99;
    printf("%d\\n", (int)value);

    //自增1,原子操作
    value++;
    printf("%d\\n", (int)value);

    return 0;
}

除此以外, std::atomic 提供了大量有用的方法

方法名方法说明
operator=存储值于原子对象
store原子地以非原子对象替换原子对象的值
load原子地获得原子对象的值
exchange原子地替换原子对象的值并获得它先前持有的值
compare_exchange_weak compare_exchange_strong原子地比较原子对象与非原子参数的值,若相等则进行交换,若不相等则进行加载
fetch_add原子地将参数加到存储于原子对象的值,并返回先前保有的值
fetch_sub原子地从存储于原子对象的值减去参数,并获得先前保有的值
fetch_and原子地进行参数和原子对象的值的逐位与,并获得先前保有的值
fetch_or原子地进行参数和原子对象的值的逐位或,并获得先前保有的值
fetch_xor原子地进行参数和原子对象的值的逐位异或,并获得先前保有的值
operator++ operator++(int) operator-- operator–(int)令原子值增加或减少一
operator += operator -= operator &= operator |= operator ^==加、减,或与原子值进行逐位与、或、异或

以上是关于整型变量的原子操作的主要内容,如果未能解决你的问题,请参考以下文章

Java原子操作类AtomicInteger应用场景

java线程安全的整型类AtomicInteger

原子性操作原理分析

linux并行与竞态

信号量与互斥锁

linux驱动程序中的并发控制(原子操作)-43