使用显式fences和std :: atomic有什么区别?

时间:2013-01-05 01:58:36

标签: c++ c++11 atomic memory-fences

假设对齐的指针加载和存储在目标平台上自然是原子的,那么它之间有什么区别:

// Case 1: Dumb pointer, manual fence
int* ptr;
// ...
std::atomic_thread_fence(std::memory_order_release);
ptr = new int(-4);

这样:

// Case 2: atomic var, automatic fence
std::atomic<int*> ptr;
// ...
ptr.store(new int(-4), std::memory_order_release);

和此:

// Case 3: atomic var, manual fence
std::atomic<int*> ptr;
// ...
std::atomic_thread_fence(std::memory_order_release);
ptr.store(new int(-4), std::memory_order_relaxed);

我的印象是它们都是等价的,但Relacy检测到数据竞争 第一种情况(仅):

struct test_relacy_behaviour : public rl::test_suite<test_relacy_behaviour, 2>
{
    rl::var<std::string*> ptr;
    rl::var<int> data;

    void before()
    {
        ptr($) = nullptr;
        rl::atomic_thread_fence(rl::memory_order_seq_cst);
    }

    void thread(unsigned int id)
    {
        if (id == 0) {
            std::string* p  = new std::string("Hello");
            data($) = 42;
            rl::atomic_thread_fence(rl::memory_order_release);
            ptr($) = p;
        }
        else {
            std::string* p2 = ptr($);        // <-- Test fails here after the first thread completely finishes executing (no contention)
            rl::atomic_thread_fence(rl::memory_order_acquire);

            RL_ASSERT(!p2 || *p2 == "Hello" && data($) == 42);
        }
    }

    void after()
    {
        delete ptr($);
    }
};

我联系了Relacy的作者,以了解这是否是预期的行为;他说在我的测试用例中确实存在数据竞争。 但是,我发现它时遇到了麻烦;谁能指出我的比赛是什么? 最重要的是,这三种情况有何区别?

更新:我突然想到,Relacy可能只是在抱怨跨线程访问的变量的原子性(或缺少,而不是)所有,它不知道我打算只是在对齐整数/指针访问自然是原子的平台上使用这个代码。

另一次更新:Jeff Preshing撰写了一篇精彩的博文explaining the difference between explicit fences and the built-in ones(“围栏”与“操作”)。案例2和3显然不相同! (无论如何,在某些微妙的情况下。)

5 个答案:

答案 0 :(得分:12)

我相信代码有竞争。案例1和案例2不等同。

29.8 [atomics.fences]

  

-2-如果存在原子操作 X Y ,则发布范围 A 与获取范围 B 同步,都在一些原子对象 M 上运行,这样 A X 之前排序, X 修改< em> M , Y B 之前排序, Y 读取由 X 写的值或者假设释放序列 X 中任何副作用所写的值,如果是释放操作,将会结束。

在案例1中,您的版本围栏不与您的获取围栏同步,因为ptr不是原子对象,ptr上的存储和加载不是原子操作。

案例2和案例3是等价的(实际上,并不完全,请参阅LWimsey的评论和回答),因为ptr是一个原子对象,而商店是一个原子操作。 ([atomic.fences]的第3和第4段描述了栅栏如何与原子操作同步,反之亦然。)

围栏的语义仅针对原子对象和原子操作进行定义。您的目标平台和实现是否提供更强的保证(例如将任何指针类型视为原子对象)是最佳实现定义。

N.B。对于案例2和案例3,ptr上的获取操作可能在商店之前发生,因此将从未初始化的atomic<int*>读取垃圾。简单地使用获取和释放操作(或围栏)并不能确保存储在加载之前发生,它只能确保如果加载读取存储的值,那么代码就会正确同步。

答案 1 :(得分:11)

答案 2 :(得分:4)

虽然各种答案涵盖了潜在问题的部分和/或提供有用的信息,但没有答案正确地描述了所有三种情况的潜在问题。

为了在线程之间同步内存操作,释放和获取障碍用于指定排序 在图中,线程1中的内存操作A不能向下移动(单向)释放屏障(无论这是否是原子存储上的释放操作, 或者是一个独立的释放栅栏,后面是一个放松的原子商店)。因此,内存操作A保证在原子存储之前发生。 线程2中的存储器操作B也是如此,它不能越过获取屏障向上移动;因此,原子载荷发生在存储器操作B之前。

enter image description here

原子ptr本身提供了基于其具有单个修改顺序的保证的线程间排序。一旦线程2看到ptr的值, 保证存储(以及内存操作A)在加载之前发生。因为负载保证在内存操作B之前发生, 传递性规则表明存储器操作A发生在B之前并且同步完成。

有了这个,让我们来看看你的3个案例。

案例1已被破坏,因为ptr是一种非原子类型,在不同的线程中被修改。这是数据竞争的经典示例,它会导致未定义的行为。

案例2是正确的。作为参数,在释放操作之前对new的整数分配进行排序。这相当于:

// Case 2: atomic var, automatic fence
std::atomic<int*> ptr;
// ...
int *tmp = new int(-4);
ptr.store(tmp, std::memory_order_release);

案例3被打破,虽然是一种微妙的方式。问题是即使ptr赋值在独立栅栏后正确排序, 整数分配(new)也在围栏之后排序,导致整数内存位置的数据竞争。

代码相当于:

// Case 3: atomic var, manual fence
std::atomic<int*> ptr;
// ...
std::atomic_thread_fence(std::memory_order_release);

int *tmp = new int(-4);
ptr.store(tmp, std::memory_order_relaxed);

如果将其映射到上图,new运算符应该是内存操作A的一部分。在释放栏下方排序, 排序保证不再保持,并且整数分配实际上可以与线程2中的存储器操作B重新排序。 因此,线程2中的load()可能会返回垃圾或导致其他未定义的行为。

答案 3 :(得分:1)

支持原子变量的内存只能用于原子的内容。但是,一个普通的变量,如案例1中的ptr,是一个不同的故事。一旦编译器有权写入它,它就可以向它写任何东西,甚至是当你用完寄存器时临时值的值。

请记住,你的例子在病理上很干净。给出一个稍微复杂的例子:

std::string* p  = new std::string("Hello");
data($) = 42;
rl::atomic_thread_fence(rl::memory_order_release);
std::string* p2 = new std::string("Bye");
ptr($) = p;

编译器选择重用指针

是完全合法的
std::string* p  = new std::string("Hello");
data($) = 42;
rl::atomic_thread_fence(rl::memory_order_release);
ptr($) = new std::string("Bye");
std::string* p2 = ptr($);
ptr($) = p;

为什么会这样做?我不知道,也许是保留缓存行或其他东西的一些奇特技巧。关键是,由于ptr在案例1中不是原子的,因此在线写'ptr($)= p'和'std :: string * p2 = ptr($)'上的读取之间存在竞争情况,产生未定义的行为。在这个简单的测试用例中,编译器可能不会选择行使这一权利,并且它可能是安全的,但在更复杂的情况下,编译器有权滥用ptr,但它很高兴,而且Relacy得到了这个。

我最喜欢的文章:http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong

答案 4 :(得分:0)

第一个例子中的比赛是在指针的发布和它指向的东西之间。原因是,您在围栏之后<=>指针的创建和初始化(=与指针的发布在同一侧):

int* ptr;    //noop
std::atomic_thread_fence(std::memory_order_release);    //fence between noop and interesting stuff
ptr = new int(-4);    //object creation, initalization, and publication

如果我们假设CPU访问正确对齐的指针是原子的,则可以通过编写以下代码来纠正代码:

int* ptr;    //noop
int* newPtr = new int(-4);    //object creation & initalization
std::atomic_thread_fence(std::memory_order_release);    //fence between initialization and publication
ptr = newPtr;    //publication

请注意,即使这在许多机器上都可以正常工作,但在C ++标准中绝对不能保证最后一行的原子性。因此,首先要更好地使用atomic<>变量。