什么是使用显式栅栏和std ::原子之间的区别?(What is the difference be

2019-07-17 21:14发布

假设对齐指针加载和存储在目标平台上自然原子,也正是这一区别:

// 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可以简单地抱怨原子 (或缺乏,相当)的跨线程访问的变量...毕竟,它不知道,我只打算使用的平台上的代码其中对准整数/指针访问是自然原子。

另一个更新 :杰夫Preshing写了一本优秀的博客文章解释明确围栏和内置者之间的区别 (“围栏”与“行动”)。 例2和3是显然并不等同! (在某些情况微妙,反正。)

Answer 1:

我相信代码有一个比赛。 案例1和2的情况下是不等价的。

29.8 [atomics.fences]

-2-甲释放围栏与获取围栏如果存在的原子操作XY同步,在某些原子物体M,使得AX之前测序两个操作,X修改M,YB之前测序,和Y读取由X或通过任何副作用在假想释放序列X写入好像它是一个释放操作将朝向一个值写入的值。

在案例1中,因为你释放围栏不与你的获取栅栏同步ptr不是一个原子对象以及存储和加载在ptr不是原子操作。

案例2和3的情况下是等价的( 实际上,不完全,看到LWimsey的意见,回答 ),因为ptr是一个原子对象和实体店是一个原子操作。 (第3和的[atomic.fences] 4描述了一个围栏与一个原子操作如何同步,反之亦然。)

围栏的语义仅相对于原子对象和原子操作定义。 无论您的目标平台和您的实现提供了强大的保证(如处理任何指针类型作为一个原子对象)是实现定义的最好的。

NB两个壳体2和壳体3上的获取操作的ptr可以在存储之前发生,因此将从未初始化读垃圾atomic<int*> 简单地使用获取和释放操作(或栅栏)不保证存储负载之前发生,它只确保负载读取所存储的值,则该代码被正确地同步。



Answer 2:

一些相关的参考资料:

  • 在C ++ 11标准草案 (PDF,见第1,29和30);
  • 汉斯-J。 在C ++并行的贝姆的概况 ;
  • 麦肯尼,Boehm和Crowl上并发在C ++ ;
  • GCC对并发性发育笔记C ++ ;
  • Linux内核上并行笔记 ;
  • 在这里#2回答一个相关的问题 ;
  • 与答案另一个相关的问题 ;
  • Cppmem,其中实验并发沙箱;
  • Cppmem的帮助页面 ;
  • 自旋,用于分析并发系统的逻辑一致性的工具;
  • 的从硬件角度存储器障碍的概述 (PDF)。

上述的一些您可能感兴趣的和其他读者。



Answer 3:

虽然各种各样的回答涵盖的潜在问题是什么和/或提供有用的信息的点点滴滴,没有回答正确地描述了这三种情况下的潜在问题。

为了线程之间的内存操作同步,发布和获取障碍用于指定排序。
在该图中,在线程1的存储器操作A不能跨越(单程)释放屏障(无论是否是在原子商店的释放操作,或一个独立的释放围栏随后松弛原子商店)向下移动。 因此,存储操作的保证原子存储之前发生。 这同样适用于在螺纹2不能穿过屏障获取向上移动存储器操作B; 因此原子负载之前的存储器操作B.发生

原子ptr本身提供了一个基于它有一个单独的修改为了保证线程间的排序。 一旦线程2看到了一个值ptr ,它保证了存储(并且因此存储器操作A)负载之前发生。 由于负载保证之前的内存操作B到发生,传递的规则说,存储操作的发生之前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.一部分,排序保证不再持有和整数分配实际上可能与内存操作的B线2被重新排序。因此,一个load()在线程2可以返回垃圾或导致其他未定义的行为。



Answer 4:

存储器备份的原子变量永远只能被用于原子的内容。 然而,一个普通的可变,如同壳体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-c​​ould-possibly-go-wrong



Answer 5:

在第一个例子中的比赛是指针的出版,它指向的东西之间。 原因是,你有围栏(=在同一侧的指针出版) 之后的创建和指针的初始化:

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<>在首位的变量。



文章来源: What is the difference between using explicit fences and std::atomic?