好处&获取条件std :: atomic_thread_fence的缺点是什么?

时间:2016-02-08 13:55:53

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

下面的代码显示了通过原子标志获取共享状态的两种方法。读者线程调用poll1()poll2()来检查作者是否已发信号通知。

民意调查选项#1:

bool poll1() {
    return (flag.load(std::memory_order_acquire) == 1);
}

民意调查选项#2:

bool poll2() {
    int snapshot = flag.load(std::memory_order_relaxed);
    if (snapshot == 1) {
        std::atomic_thread_fence(std::memory_order_acquire);
        return true;
    }
    return false;
}

请注意,选项#1为presented in an earlier question,选项#2类似于example code at cppreference.com

假设读者同意仅在poll函数返回true时检查共享状态,那么两个poll函数是否正确且等效?

选项#2是否有标准名称?

每个选项有哪些优点和缺点?

选项#2在实践中可能更有效吗?是否有可能降低效率?

以下是一个完整的工作示例:

#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>

int x; // regular variable, could be a complex data structure

std::atomic<int> flag { 0 };

void writer_thread() {
    x = 42;
    // release value x to reader thread
    flag.store(1, std::memory_order_release);
}

bool poll1() {
    return (flag.load(std::memory_order_acquire) == 1);
}

bool poll2() {
    int snapshot = flag.load(std::memory_order_relaxed);
    if (snapshot == 1) {
        std::atomic_thread_fence(std::memory_order_acquire);
        return true;
    }
    return false;
}

int main() {
    x = 0;

    std::thread t(writer_thread);

    // "reader thread" ...  
    // sleep-wait is just for the test.
    // production code calls poll() at specific points

    while (!poll2()) // poll1() or poll2() here
      std::this_thread::sleep_for(std::chrono::milliseconds(50));

    std::cout << x << std::endl;

    t.join();
}

1 个答案:

答案 0 :(得分:2)

我想我可以回答你的大多数问题。

两种选择当然都是正确的,但它们并不完全相同,因为独立栅栏的适用性稍大(它们在您想要完成的任务方面相当,但是独立栅栏在技术上可以适用于其他事情 - 想象一下这段代码是否内联)。 this post by Jeff Preshing中解释了独立围栏与商店/取景围栏的不同之处。

据我所知,选项#2中的check-then-fence模式没有名称。但这并不罕见。

在性能方面,使用x64(Linux)上的g ++ 4.8.1,两个选项生成的程序集归结为单个加载指令。这并不奇怪,因为x86(-64)加载和存储都在硬件级别具有获取和释放语义(x86以其非常强大的内存模型而闻名)。

但是,对于ARM,内存屏障会编译为实际的单个指令,会生成以下输出(使用带有-O3 -DNDEBUG的{​​{3}}):

while (!poll1());

.L25:
    ldr     r0, [r2]
    movw    r3, #:lower16:.LANCHOR0
    dmb     sy
    movt    r3, #:upper16:.LANCHOR0
    cmp     r0, #1
    bne     .L25

while (!poll2());

.L29:
    ldr     r0, [r2]
    movw    r3, #:lower16:.LANCHOR0
    movt    r3, #:upper16:.LANCHOR0
    cmp     r0, #1
    bne     .L29
    dmb     sy

您可以看到唯一的区别是放置同步指令(dmb)的位置 - 在poll1的循环内部,以及在poll2之后。所以poll2在这个真实世界的情况下确实更有效率:-)(但是请进一步阅读为什么这可能无关紧要,如果它们在循环中被调用以阻止直到标志发生变化。)

对于ARM64,输出是不同的,因为存在内置屏障的特殊加载/存储指令(ldar - &gt; load-acquire)。

while (!poll1());

.L16:
    ldar    w0, [x1]
    cmp     w0, 1
    bne     .L16

while (!poll2());

.L24:
    ldr     w0, [x1]
    cmp     w0, 1
    bne     .L24
    dmb     ishld

同样,poll2导致一个没有障碍的循环,一个在外面,而poll1每次都会形成障碍。

现在,哪一个实际上更高性能需要运行基准测试,不幸的是我没有为此设置。反直觉地,poll1poll2在这种情况下可能最终同样有效,因为如果标志变量是一个,花费额外的时间等待记忆效应在循环内传播可能实际上不会浪费时间那些需要传播的效果(即,即使对poll1的个别(内联)调用比poll2调用的时间长,循环退出所需的总时间也可能相同)。当然,这是假设一个循环等待标志改变 - 单独调用poll1 需要比单独调用poll2更多的工作。

所以,我认为总的来说,只要编译器可以消除分支,poll2永远不会比poll1效率低得多,并且通常可以更快,这是相当安全的。当它被内联时(至少这三种流行的架构似乎就是这种情况)。

我的(略有不同)测试代码供参考:

#include <atomic>
#include <thread>
#include <cstdio>

int sharedState;
std::atomic<int> flag(0);

bool poll1() {
    return (flag.load(std::memory_order_acquire) == 1);
}

bool poll2() {
    int snapshot = flag.load(std::memory_order_relaxed);
    if (snapshot == 1) {
        std::atomic_thread_fence(std::memory_order_acquire);
        return true;
    }
    return false;
}

void __attribute__((noinline)) threadFunc()
{
    while (!poll2());
    std::printf("%d\n", sharedState);
}

int main(int argc, char** argv)
{
    std::thread t(threadFunc);
    sharedState = argc;
    flag.store(1, std::memory_order_release);
    t.join();
    return 0;
}