【C++11并发】何为std::atomic,其原理如何,以及使用注意事项

2021/4/9 22:25:55

本文主要是介绍【C++11并发】何为std::atomic,其原理如何,以及使用注意事项,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

对原子类型的某些操作不一定是线程安全的,比如说
a=a+12; 非线程安全
a+=12; 线程安全
分析如下

Cppreference介绍

成员函数(原子操作)

is_lock_free
store用非原子参数替换原子对象的值
load获取原子对象的值
exchange交换两个原子对象的值
wait阻塞线程,直到收到通知并且原子值发生变化
notify_one通知至少一个线程在等待原子对象
notify_all通知所有阻塞的线程等待原子对象

特殊的成员函数(原子操作)

fetch_add将参数添加到存储在原子对象中的值中,并获取前面保存的值
fetch_sub类似
fetch_and类似
fetch_or类似
fetch_xor类似

相关重载运算符(原子操作)

image.png

What exactly is std::atomic?

我知道std::atomic<>是一个原子物体。但是原子能达到什么程度?据我所知,操作可以是原子的。使物体成为原子的确切含义是什么?例如,如果有两个线程同时执行以下代码:
a=a+12;
那么整个操作(比如add_twelve_to(int)是原子的吗?还是对可变原子进行了更改?


ANSWER:

std::atomic<>的每个实例化和完全特化表示一种类型,不同的线程可以同时操作(该类型实例),而不会引发未定义的行为:

原子类型的对象是唯一没有数据竞争的c++对象;也就是说,如果一个线程写一个原子对象,而另一个线程从它读取,这种行为是定义良好的。
此外,对原子对象的访问可以建立线程间同步,并按std::memory_order指定的顺序对非原子内存访问进行排序。

std::atomic<>所包装的操作,在c++11前的时代,必须使用msvc的互锁函数或GCC内置的atomic bultins;
另外,std::atomic<>通过允许指定同步和排序约束的各种内存顺序,为您提供了更多的控制。如果您想阅读更多关于C++ 11原子和内存模型的内容,这些链接可能是有用的:

  • C++原子与内存排序
  • 比较:在C++ 11中使用原子锁与互斥锁和RW锁的无锁编程
  • C++ 11引入了标准化内存模型。这是什么意思?它会如何影响C++程序设计?
  • C++ 11中的并发性

注意,对于典型的用例,可使用重载运算符或另一些集合

std::atomic<long> value(0);
value++; //This is an atomic op
value += 5; //And so is this

因为运算符语法不允许您指定内存顺序,故这些操作可配合 std::memory_order_seq_cst来操作,其为C++ 11中所有原子操作的默认顺序。它保证所有原子操作之间的顺序一致性(全局排序)。
然而,在某些情况下,可以使用更明确的形式:

std::atomic<long> value {0};
value.fetch_add(1, std::memory_order_relaxed); // Atomic, but there are no synchronization or ordering constraints
value.fetch_add(5, std::memory_order_release); // Atomic, performs 'release' operation

现在,您的示例:
a=a+12;
执行顺序如下:

std::memory_order_seq_cst默认在此处使用

  1. 调用a.load();
  2. 12+a.load();
  3. 调用a.store();

但若将示例改为a+=12,则将为原子操作(因此+=重载了),上述操作大致与如下相等
a.fetch_add(12,std::memory_order_seq_cst)
关于你的评论:

A regular int has atomic loads and stores. Whats the point of wrapping it with atomic<>?

你的声明只适用于为存储和/或加载提供原子性保证的架构,但存在架构不保证上述操作的原子性。通常要求必须对与字/双字对齐的地址执行操作才能使其为原子的。但使用std::atomic,可使用与任何平台并保持操作的原子性。

/* 使用std::atomic允许如下代码 */
void* sharedData = nullptr;
std::atomic<int> ready_flag = 0;

// Thread 1
void produce()
{
    sharedData = generateData();
    ready_flag.store(1, std::memory_order_release);
}

// Thread 2
void consume()
{
    while (ready_flag.load(std::memory_order_acquire) == 0)
    {
        std::this_thread::yield();
    }

    assert(sharedData != nullptr); // will never trigger
    processData(sharedData);
}

注意,断言条件将始终为真(因此永远不会触发),因此您可以始终确保,在while循环退出后数据已就绪。这是因为:

  • store()的调用晚于sharedData的赋值(假设赋值均有效),并且使用顺序std::memory_order_release

memory_order_release
具有这种内存顺序的store操作执行释放操作:当前线程中的任何读或写操作都不能在store之后重新排序。当前线程中的所有写操作在其他获得相同原子变量的线程中都可见

  • sharedDatawhile循环退出后使用,故在load()后,其会返回一个非0值,load()使用std::memory_order_acquire

std::memory_order_acquire
具有此内存顺序的load在受影响的内存位置上执行获取操作:当前线程中的任何读或写操作都不能在load()之前重新排序。释放相同原子变量的其他线程中的所有写操作都在当前线程中可见。

这使您能够精确地控制同步,并允许您显式地指定代码 可能/可能不 /将会/将不会的行为。如果只保证原子性本身,这是不可能的。尤其是当涉及到非常有趣的同步模型时,比如 release-consume ordering.



这篇关于【C++11并发】何为std::atomic,其原理如何,以及使用注意事项的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程