首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >究竟什么是std::原子?

究竟什么是std::原子?
EN

Stack Overflow用户
提问于 2015-08-13 02:00:06
回答 3查看 221.8K关注 0票数 251

我知道std::atomic<>是一个原子物体。但是原子在多大程度上?据我所知,一个操作可以是原子的。使物体原子化究竟是什么意思?例如,如果有两个线程并发执行以下代码:

代码语言:javascript
复制
a = a + 12;

那么整个操作(比如add_twelve_to(int))是原子的吗?还是对可变原子(所以是operator=())进行了更改?

EN

回答 3

Stack Overflow用户

回答已采纳

发布于 2015-08-13 02:55:09

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

原子类型的对象是唯一不受数据竞争影响的C++对象;也就是说,如果一个线程写入一个原子对象,而另一个线程从它读取数据,则该行为是很好定义的。 此外,对原子对象的访问可以建立线程间同步,并按照std::memory_order指定的顺序对非原子内存访问进行排序。

std::atomic<>包装操作,在C++前11次,在GCC的情况下,必须使用(例如) 互锁函数与MSVC或原子球来执行。

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

注意,对于典型的用例,您可能会使用过载算术算子另一套

代码语言:javascript
复制
std::atomic<long> value(0);
value++; //This is an atomic op
value += 5; //And so is this

由于运算符语法不允许您指定内存顺序,这些操作将使用std::memory_order_seq_cst执行,因为这是C++ 11中所有原子操作的默认顺序。它保证了所有原子操作之间的顺序一致性(全局排序)。

但是,在某些情况下,这可能不是必需的(没有免费的),因此您可能希望使用更显式的形式:

代码语言:javascript
复制
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

现在,你的例子:

代码语言:javascript
复制
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<>包装它有什么意义?

您的语句只适用于为存储和/或加载提供原子性保证的体系结构。有些体系结构不这样做。此外,通常要求必须在对word/dword对齐的地址上执行操作,才能成为原子std::atomic<>,这在每个平台上都是原子的,而不需要额外的要求。此外,它还允许您编写这样的代码:

代码语言:javascript
复制
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);
}

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

  • 对标志的store()是在设置sharedData之后执行的(我们假设generateData()总是返回有用的东西,特别是从不返回NULL),并使用std::memory_order_release顺序:

memory_order_release 具有此内存顺序的存储操作执行发布操作:当前线程中的任何读或写都不能在存储后重新排序。在当前线程中的所有写入在获得相同原子变量的其他线程中都是可见的。

  • sharedDatawhile循环退出后使用,因此在load() from标志之后将返回一个非零值。load()使用std::memory_order_acquire顺序:

std::memory_order_acquire 具有此内存顺序的加载操作在受影响的内存位置上执行获取操作:在加载当前线程之前,不能在之前重新排序中的读或写。在释放相同原子变量的其他线程中都可以在当前线程中看到。

这给了您对同步的精确控制,并允许您显式地指定代码可能/可能不会/将/不会行为的方式。如果只是保证原子性本身,这是不可能的。特别是当涉及到非常有趣的同步模型,如释放-消耗顺序

票数 279
EN

Stack Overflow用户

发布于 2019-11-17 19:40:02

std::atomic 的存在是因为许多ISAs对it有直接的硬件支持()。

C++标准对std::atomic的描述已经在其他答案中进行了分析。

现在让我们看看std::atomic编译了什么来获得一种不同的洞察力。

本实验的主要优点是现代CPU直接支持原子整数运算,例如x86中的锁前缀,而std::atomic基本上作为可移植接口存在于这些导入:在x86程序集中,“锁”指令是什么意思?在aarch64中,LDADD将被使用。

这种支持允许更快地替代更一般的方法,例如std::mutex,它可以使更复杂的多指令部分原子化,而代价是比std::atomic慢,因为std::mutex在Linux中使futex系统调用比std::atomic发出的用户指令慢得多,请参见:是不是:互斥创造了一道栅栏?

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

main.cpp

代码语言:javascript
复制
#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上游

编译、运行和拆卸:

代码语言:javascript
复制
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竞赛条件输出

代码语言:javascript
复制
expect 400000
global 100000

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

代码语言:javascript
复制
expect 400000
global 400000

main_fail.out的拆卸

代码语言:javascript
复制
   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的拆卸

代码语言:javascript
复制
   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的拆卸

代码语言:javascript
复制
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。锁前缀使以下inc自动获取、修改和更新内存。
  • 我们的显式内联程序集锁前缀编译成几乎与std::atomic相同的东西,只是我们的inc被使用而不是add。不知道为什么GCC选择add,因为我们的公司产生了一个更小的解码1字节。

ARMv8可以在较新的CPU中使用LDAXR + STLXR或LDADD:如何在普通C中启动线程?

测试在Ubuntu 19.10 AMD64,GCC 9.2.1,联想ThinkPad P51。

票数 32
EN

Stack Overflow用户

发布于 2015-08-13 02:42:17

据我所知,std::atomic<>使一个对象原子化。

那是透视的问题..。您不能将其应用于任意对象,并使它们的操作成为原子操作,但是可以使用(大多数)整数类型和指针提供的专门化。

a = a + 12;

std::atomic<>不(使用模板表达式)将其简化为单个原子操作,而是由operator T() const volatile noexcept成员执行原子load() of a,然后添加12,operator=(T t) noexcept执行store(t)

票数 21
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/31978324

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档