什么是std :: atomic?

时间:2015-08-13 02:00:06

标签: c++ multithreading c++11 atomic

我知道std::atomic<>是一个原子对象。但原子到什么程度?根据我的理解,操作可以是原子的。使对象成为原子意味着什么?例如,如果有两个线程同时执行以下代码:

a = a + 12;

然后是整个操作(比如说add_twelve_to(int))原子?或者是变量原子的变化(所以operator=())?

3 个答案:

答案 0 :(得分:109)

std::atomic<>的每个实例化和完全特化都代表一种类型,不同的线程可以同时对它们的实例进行操作,而不会引发未定义的行为:

  

原子类型的对象是唯一没有数据竞争的C ++对象;也就是说,如果一个线程写入原子对象而另一个线程从中读取,则行为是明确定义的。

     

此外,对原子对象的访问可以建立线程间同步并按std::memory_order指定的顺序进行非原子内存访问。

std::atomic<>包装操作,在C ++之前的11次中,必须使用(例如)interlocked functions与MSVC或atomic bultins在GCC的情况下执行。

此外,std::atomic<>通过允许指定同步和排序约束的各种memory orders为您提供更多控制。如果您想了解有关C ++ 11原子和内存模型的更多信息,这些链接可能很有用:

请注意,对于典型用例,您可能会使用overloaded arithmetic operatorsanother set of them

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;

不会评估为单个原子操作:它将导致a.load()(原子本身),然后在此值与12a.store()(也是原子)之间相加最后结果。如前所述,此处将使用std::memory_order_seq_cst

但是,如果你写a += 12,它将是一个原子操作(如前所述),大致相当于a.fetch_add(12, std::memory_order_seq_cst)

至于你的评论:

  

常规int具有原子载荷和存储。是什么意思用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执行该标志(我们假设,generateData()始终返回有用的内容,特别是永不返回NULL)并使用{ {1}}订单:
  

std::memory_order_release

     

具有此内存订单的商店操作会执行发布   操作:当前线程中没有读取或写入可以重新排序    此商店后 当前线程中的所有写入都可见   获取相同原子变量的其他线程

    memory_order_release循环退出后使用
  • sharedData,因此在flag while之后将返回非零值。 load()使用load()订单:
  

std::memory_order_acquire

     

具有此内存顺序的加载操作会执行获取操作   在受影响的内存位置:当前没有读取或写入   线程可以重新排序之前此加载。 所有写入其他线程   释放相同的原子变量在当前可见   螺纹

这使您可以精确控制同步,并允许您明确指定代码可能/可能不会/将要/不会表现的方式。如果只保证是原子性本身,这是不可能的。特别是涉及非常有趣的同步模型,如release-consume ordering

答案 1 :(得分:15)

  

我理解std::atomic<>使对象成为原子。

这是一个透视问题......你不能将它应用于任意对象并使它们的操作变为原子,但是可以使用为(大多数)整数类型和指针提供的特化。

  

a = a + 12;

std::atomic<>没有(使用模板表达式)将此简化为单个原子操作,而operator T() const volatile noexcept成员执行load()的原子a,然后十二是已添加,operator=(T t) noexcept执行了store(t)

答案 2 :(得分:-2)

我们来分解一些东西

C ++标准关于std::atomic的说法在其他答案中得到了分析。

因此,现在让我们来看看std::atomic所编译得到的东西,以获得另一种见解。

此实验的主要成果是现代CPU直接支持原子整数操作,例如x86中的LOCK前缀,并且std::atomic主要作为这些指令的可移植接口存在:What does the "lock" instruction mean in x86 assembly?在aarch64中,将使用LDADD

此支持允许更快地替代更通用的方法,例如std::mutex,这可以使更复杂的多指令节成为原子,其代价是比std::atomic慢,从而使futex系统调用在Linux中,另请参见:Does std::mutex create a fence?

让我们考虑下面的多线程程序,该程序在多个线程之间增加全局变量,并根据所使用的预处理器定义使用不同的同步机制。

main.cpp

#include <atomic>
#include <iostream>
#include <thread>
#include <vector>

size_t niters;

#if STD_ATOMIC
std::atomic_ulong global(0);
#else
uint64_t global = 0;
#endif

void threadMain() {
    for (size_t i = 0; i < niters; ++i) {
#if LOCK
        __asm__ __volatile__ (
            "lock incq %0;"
            : "+m" (global),
              "+g" (i) // to prevent loop unrolling
            :
            :
        );
#else
        __asm__ __volatile__ (
            ""
            : "+g" (i) // to prevent he loop from being optimized to a single add
            : "g" (global)
            :
        );
        global++;
#endif
    }
}

int main(int argc, char **argv) {
    size_t nthreads;
    if (argc > 1) {
        nthreads = std::stoull(argv[1], NULL, 0);
    } else {
        nthreads = 2;
    }
    if (argc > 2) {
        niters = std::stoull(argv[2], NULL, 0);
    } else {
        niters = 10;
    }
    std::vector<std::thread> threads(nthreads);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i] = std::thread(threadMain);
    for (size_t i = 0; i < nthreads; ++i)
        threads[i].join();
    uint64_t expect = nthreads * niters;
    std::cout << "expect " << expect << std::endl;
    std::cout << "global " << global << std::endl;
}

GitHub upstream

编译,运行和反汇编:

comon="-ggdb3 -O3 -std=c++11 -Wall -Wextra -pedantic main.cpp -pthread"
g++ -o main_fail.out                    $common
g++ -o main_std_atomic.out -DSTD_ATOMIC $common
g++ -o main_lock.out       -DLOCK       $common

./main_fail.out       4 100000
./main_std_atomic.out 4 100000
./main_lock.out       4 100000

gdb -batch -ex "disassemble threadMain" main_fail.out
gdb -batch -ex "disassemble threadMain" main_std_atomic.out
gdb -batch -ex "disassemble threadMain" main_lock.out

main_fail.out的输出极有可能是“错误”:

expect 400000
global 100000

和其他人的确定性“正确”输出:

expect 400000
global 400000

拆卸main_fail.out

   0x0000000000002780 <+0>:     endbr64 
   0x0000000000002784 <+4>:     mov    0x29b5(%rip),%rcx        # 0x5140 <niters>
   0x000000000000278b <+11>:    test   %rcx,%rcx
   0x000000000000278e <+14>:    je     0x27b4 <threadMain()+52>
   0x0000000000002790 <+16>:    mov    0x29a1(%rip),%rdx        # 0x5138 <global>
   0x0000000000002797 <+23>:    xor    %eax,%eax
   0x0000000000002799 <+25>:    nopl   0x0(%rax)
   0x00000000000027a0 <+32>:    add    $0x1,%rax
   0x00000000000027a4 <+36>:    add    $0x1,%rdx
   0x00000000000027a8 <+40>:    cmp    %rcx,%rax
   0x00000000000027ab <+43>:    jb     0x27a0 <threadMain()+32>
   0x00000000000027ad <+45>:    mov    %rdx,0x2984(%rip)        # 0x5138 <global>
   0x00000000000027b4 <+52>:    retq

拆卸main_std_atomic.out

   0x0000000000002780 <+0>:     endbr64 
   0x0000000000002784 <+4>:     cmpq   $0x0,0x29b4(%rip)        # 0x5140 <niters>
   0x000000000000278c <+12>:    je     0x27a6 <threadMain()+38>
   0x000000000000278e <+14>:    xor    %eax,%eax
   0x0000000000002790 <+16>:    lock addq $0x1,0x299f(%rip)        # 0x5138 <global>
   0x0000000000002799 <+25>:    add    $0x1,%rax
   0x000000000000279d <+29>:    cmp    %rax,0x299c(%rip)        # 0x5140 <niters>
   0x00000000000027a4 <+36>:    ja     0x2790 <threadMain()+16>
   0x00000000000027a6 <+38>:    retq   

拆卸main_lock.out

Dump of assembler code for function threadMain():
   0x0000000000002780 <+0>:     endbr64 
   0x0000000000002784 <+4>:     cmpq   $0x0,0x29b4(%rip)        # 0x5140 <niters>
   0x000000000000278c <+12>:    je     0x27a5 <threadMain()+37>
   0x000000000000278e <+14>:    xor    %eax,%eax
   0x0000000000002790 <+16>:    lock incq 0x29a0(%rip)        # 0x5138 <global>
   0x0000000000002798 <+24>:    add    $0x1,%rax
   0x000000000000279c <+28>:    cmp    %rax,0x299d(%rip)        # 0x5140 <niters>
   0x00000000000027a3 <+35>:    ja     0x2790 <threadMain()+16>
   0x00000000000027a5 <+37>:    retq

结论:

  • 非原子版本将全局变量保存到寄存器,并递增寄存器。

    因此,最后,很有可能发生四次写回全局且具有相同100000“错误”值的情况。

  • std::atomic编译为lock addq。 LOCK前缀使以下inc自动获取,修改和更新内存。

  • 我们的显式内联汇编LOCK前缀编译为与std::atomic几乎相同,除了使用我们的inc代替add。考虑到我们的INC产生的解码小1字节,因此不确定为什么GCC选择add

ARMv8可以在以下CPU中使用LDAXR + STLXR或LDADD:How do I start threads in plain C?

联想ThinkPad P51在Ubuntu 19.10 AMD64中进行了测试。