41

我试图理解 c++11 中的内存栅栏,我知道有更好的方法可以做到这一点,原子变量等等,但想知道这种用法是否正确。我意识到这个程序没有做任何有用的事情,我只是想确保栅栏函数的使用符合我的想法。

基本上,该版本确保在围栏之前在此线程中所做的任何更改对围栏之后的其他线程都是可见的,并且在第二个线程中,对变量的任何更改在围栏之后的线程中立即可见?

我的理解正确吗?还是我完全错过了重点?

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

int a;

void func1()
{
    for(int i = 0; i < 1000000; ++i)
    {
        a = i;
        // Ensure that changes to a to this point are visible to other threads
        atomic_thread_fence(std::memory_order_release);
    }
}

void func2()
{
    for(int i = 0; i < 1000000; ++i)
    {
        // Ensure that this thread's view of a is up to date
        atomic_thread_fence(std::memory_order_acquire);
        std::cout << a;
    }
}

int main()
{
    std::thread t1 (func1);
    std::thread t2 (func2);

    t1.join(); t2.join();
}
4

2 回答 2

44

您的使用实际上并不能确保您在评论中提到的内容。也就是说,您对栅栏的使用并不能确保您的分配对a其他线程可见,或者您从中读取的值a是“最新的”。这是因为,尽管您似乎对应该在哪里使用栅栏有了基本的想法,但您的代码实际上并不满足这些栅栏“同步”的确切要求。

这是一个不同的例子,我认为它更好地展示了正确的用法。

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

std::atomic<bool> flag(false);
int a;

void func1()
{
    a = 100;
    atomic_thread_fence(std::memory_order_release);
    flag.store(true, std::memory_order_relaxed);
}

void func2()
{
    while(!flag.load(std::memory_order_relaxed))
        ;

    atomic_thread_fence(std::memory_order_acquire);
    std::cout << a << '\n'; // guaranteed to print 100
}

int main()
{
    std::thread t1 (func1);
    std::thread t2 (func2);

    t1.join(); t2.join();
}

atomic 标志上的加载和存储不同步,因为它们都使用宽松的内存排序。如果没有栅栏,这段代码将是数据竞争,因为我们在不同线程中对非原子对象执行冲突操作,如果没有栅栏和它们提供的同步,则a.

然而,有了栅栏,我们确实获得了同步,因为我们已经保证线程 2 将读取线程 1 写入的标志(因为我们循环直到看到该值),并且因为原子写入发生在释放栅栏之后并且原子读取发生- 在获取围栏之前,围栏同步。(具体要求见§ 29.8/2。)

这种同步意味着任何发生的事情——在发布栅栏发生之前——在任何事情发生之前——在获取栅栏之后。因此,非原子写入a发生在非原子读取之前a

当您在循环中编写变量时,事情会变得更加棘手,因为您可能会为某些特定的迭代而不是其他迭代建立先发生关系,从而导致数据竞争。

std::atomic<int> f(0);
int a;

void func1()
{
    for (int i = 0; i<1000000; ++i) {
        a = i;
        atomic_thread_fence(std::memory_order_release);
        f.store(i, std::memory_order_relaxed);
    }
}

void func2()
{
    int prev_value = 0;
    while (prev_value < 1000000) {
        while (true) {
            int new_val = f.load(std::memory_order_relaxed);
            if (prev_val < new_val) {
                prev_val = new_val;
                break;
            }
        }

        atomic_thread_fence(std::memory_order_acquire);
        std::cout << a << '\n';
    }
}

此代码仍会导致栅栏同步,但不会消除数据竞争。例如,如果f.load()碰巧返回 10,那么我们知道a=1, a=2, ...a=10都发生在该特定之前cout<<a,但我们知道cout<<a发生之前a=11。这些是不同线程上的冲突操作,没有发生之前的关系;一场数据竞赛。

于 2012-11-29T19:38:03.523 回答
7

您的用法是正确的,但不足以保证任何有用的东西。

例如,如果编译器a = i;想要这样,编译器可以自由地在内部实现:

 while(a != i)
 {
    ++a;
    atomic_thread_fence(std::memory_order_release);
 }

所以另一个线程可能会看到任何值。

当然,编译器永远不会实现这样的简单赋值。但是,在某些情况下,类似的令人困惑的行为实际上是一种优化,因此依赖以任何特定方式在内部实现的普通代码是一个非常糟糕的主意。这就是为什么我们有像原子操作这样的东西,而栅栏在与此类操作一起使用时只会产生有保证的结果。

于 2012-11-29T18:43:12.720 回答