当前位置: 首页 > 工具软件 > Atomic > 使用案例 >

boost | 线程与并发(一)atomic

章涵蓄
2023-12-01

目录

一、并发与并行

二、原子操作

三、atomic

1.创建atomic对象

2.存取atomic内部的值

3.交换值

(1)exchange()

(2)compare_exchange_weak()和compare_exchange_strong()

4.storage()

 5.整数用法

(1)atomic<int>

(2)atomic<bool>

四、并发顺序⼀致性


一、并发与并行

并发:一个处理器上跑一个进程(多个线程),轮流启用时间片。

并行:多个处理器同时各跑一个线程/进程。

生动讲解可看当我们在说“并发、多线程”,说的是什么? - 知乎 (zhihu.com)

二、原子操作

        所谓的原子操作,取的就是“原子是最小的、不可分割的最小个体”的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。

        在以往的C++标准中并没有对原子操作进行规定,我们往往是使用汇编语言,或者是借助第三方的线程库,例如intel的pthread来实现。在新标准C++11,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如,atomic_bool,atomic_int等等,如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证,多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。

三、atomic

        atomic实现了C++标准定义的原⼦操作库(C++11.29),它封装了不同计算机硬件的底层操作原语,提供了跨平台的原⼦操作功能,让我们完全摆脱了并发竞争读写变量的困扰。

        atomic库定义了基本模板类atomic<T>, atomic可以把对类型T的操作原⼦化,但不是任意的类型都可以原⼦化的,类型T如下:

  • 标量类型(scalar),如C++内建的算术类型、枚举、指针。
  • 只有平凡(trivial)拷⻉/转移构造、赋值和析构函数的类,并且可以使⽤memcmp执⾏⽐较操 作——通常这样的类都是POD。

        atomic<T>还针对整数类型和指针类型进⾏了特化,增加了⼀些特殊的操作:fetch_xxx。

        为了⽅便使⽤,atomic库还为这些原⼦化的整数和指针类型定义了typedef,它们的名字都以 “atomic_”为前缀(注意没有float和double)。

typedef atomic< char > atomic_char;
typedef atomic< unsigned char > atomic_uchar;
typedef atomic< signed char > atomic_schar;
typedef atomic< uint8_t > atomic_uint8_t;
typedef atomic< int8_t > atomic_int8_t;
typedef atomic< unsigned short > atomic_ushort;
typedef atomic< short > atomic_short;
typedef atomic< uint16_t > atomic_uint16_t;
typedef atomic< int16_t > atomic_int16_t;
typedef atomic< unsigned int > atomic_uint;
typedef atomic< int > atomic_int;
typedef atomic< uint32_t > atomic_uint32_t;
typedef atomic< int32_t > atomic_int32_t;
typedef atomic< unsigned long > atomic_ulong;
typedef atomic< long > atomic_long;
typedef atomic< uint64_t > atomic_uint64_t;
typedef atomic< int64_t > atomic_int64_t;
...

        atomic的⼤部分成员函数返回的都不是左值,这意味着它与⾮原⼦化的类型有明显的不同。

1.创建atomic对象

        有两种⽅式创建atomic对象:

  • 有参数的构造函数创建有初值的atomic;
  • ⽆参数的默认构造函数创建⼀个初值不确定的atomic(这种⽅式很危险,我们应当尽量避免。与标准库不同:标准库提供atomic_init函数可初始化对象)
    #include <atomic>
    //标准库可以使用atomic_init来初始化
    std::atomic<int> a;
    std::atomic_init<int>(&a, 10);

2.存取atomic内部的值

        atomic最重要的两个成员函数是store()和load(),它们以原⼦的⽅式存取atomic内部的值,不会因并发访问导致数据不⼀致。atomic还使⽤操作符重载简化了store()和load()的调⽤⽅式:赋值等价于store(),⽽隐式类型转换则等价于load()。

    #include <boost/atomic.hpp>
    boost::atomic_int c(20);
    //store load
    c.store(5, boost::memory_order_seq_cst);
    std::cout << "隐式类型转换:" << c << std::endl;   //隐式类型转换,相当于c.load()
    std::cout << "使用load()取值:" << c.load() << std::endl;
    //结果:
    //隐式类型转换:5
    //使用load()取值:5

3.交换值

(1)exchange()

      对于exchange()函数,顾名思义,它原⼦化地“交换”两个值,在存值之后返回内部原有的值。

    std::cout << "exchange前的值:" << c.exchange(15) << std::endl;
    std::cout << "exchange后的值:" << c << std::endl;
    //exchange前的值:5
    //exchange后的值:15

(2)compare_exchange_weak()和compare_exchange_strong()

        compare_exchange_weak()和compare_exchange_strong()是exchange()的增强版本,也就是常说的CAS(compare-and-swap)操作。它们⽐较expected,如果相等则存值为 desired,返回true/false表示原值是否被修改,但⽆论怎样,最后在expected变量⾥都会输出原 值。

        两者的区别是compare_exchange_weak()的执⾏速度快,但使⽤它即使执⾏成功也可能 会返回false。

    int expected = 14;
    bool ischange = c.compare_exchange_weak(expected, 16);
    if (ischange)
    {
        std::cout << "compare_exchange_weak后的值:" << c << std::endl;
    }
    else {
        std::cout << "compare_exchange_weak failed" << std::endl;
    }
    std::cout << "expected:" << expected << std::endl;
    //compare_exchange_weak failed
    //expected:15
    expected = 15;
    ischange = c.compare_exchange_strong(expected, 16);
    if (ischange)
    {
        std::cout << "compare_exchange_strong后的值:" << c << std::endl;
    }
    else {
        std::cout << "compare_exchange_strong failed" << std::endl;
    }
    std::cout << "expected:" << expected << std::endl;
    //compare_exchange_strong后的值:16
    //expected:15

4.storage()

        atomic的成员函数storage()可以直接获得atomic内部值的引⽤,它能够以任意⽅式操作数据, 但它也因此⽆法提供原⼦保证,在并发环境⾥我们尽量不要使⽤它。使用时需要添加宏定义:

#define BOOST_ATOMIC_SILENCE_STORAGE_DEPRECATION 

 5.整数用法

        整数和bool类型是程序中最常⽤的数据类型,原⼦化后的atomic<I>和atomic<bool>的⽤法和原始类型类似,可以安全地将它们⽤在并发环境⾥,它们能够被多个CPU核⼼或线程并发访问⽽ ⽆须特意使⽤保护⼿段,从⽽担当计数器或标志位的⻆⾊。

(1)atomic<int>

        atomic<I>是⼀类特殊的atomic对象,它除了具有基本的原⼦操作,还有整数特有的⼀些 fetch_xxx操作,它可以执⾏对应的数学运算,然后返回原值⽽不是运算后的值。atomic<I>也 重载了“++”“+=”等操作符,这些操作符重载函数内部调⽤了fetch_xxx函数,但返回运算后的值。

    boost::atomic<int> d(100);
    int value = d.fetch_add(1);
    std::cout <<"原值:"<< value << std::endl;
    std::cout <<"fetch_add后的值:"<< d << std::endl;
    value = ++d;
    std::cout <<"前置++后的值:"<< value << std::endl;
    //原值:100
    //fetch_add后的值:101
    //前置++后的值:102

(2)atomic<bool>

        atomic<bool>⽐atomic<I>更特殊⼀些,虽然它也属于整数,但它只有true/false两个取值,所以它没有fetch_xxx操作,也没有操作符重载,其接⼝与基本的atomic<T>相同(但内部实现不同)。

    boost::atomic<bool> _bool(false);
    auto x = _bool.exchange(true);
    std::cout <<"原值:"<< x << std::endl;
    std::cout <<"exchange后的值:"<< _bool << std::endl;
    //原值:0
    //exchange后的值:1

四、并发顺序⼀致性

 (可参考文章:C++ 原子操作(6种原子顺序)_小乌龟的博客-CSDN博客_c++ 原子操作

        在现代多CPU核⼼并发的环境⾥,编译器和CPU的优化都有可能打乱指令的执⾏顺序,虽然这可能会获得更⾼的执⾏效率,但也可能产⽣副作⽤,导致程序的流程不⼀定按照代码的顺序执⾏。

        atomic库在头⽂件<boost/memory_order.hpp>⾥定义了内存访问顺序的概念,它是⼀个简单的枚举类型,允许⽤户⾃⼰控制代码顺序的⼀致性。

        atomic<T>的每个成员函数都有⼀个memory_order默认参数(操作符重载函数除 外),它指定了原⼦操作的内存访问顺序要求。 memory_order参数的默认值是memory_order_seq_cst,它是最严格的顺序⼀致性的约束,不允许编译器或CPU核⼼为优化⽽调整代码或指令的执⾏顺序,保证在并发环境⾥任何CPU核⼼“看到”的指令顺序都是相同的,其程序的执⾏与单CPU单线程时相同,简化了程序员的⼯作。

enum class memory_order : unsigned int
{
    relaxed = 0,
    consume = 1,
    acquire = 2,
    release = 4,
    acq_rel = 6, // acquire | release
    seq_cst = 14 // acq_rel | 8
};

 类似资料: